    001package net.minecraftforge.common;
    002
    003import java.io.File;
    004import java.util.ArrayList;
    005import java.util.Arrays;
    006import java.util.BitSet;
    007import java.util.HashSet;
    008import java.util.Hashtable;
    009import java.util.List;
    010import java.util.ListIterator;
    011import java.util.Map;
    012import java.util.Map.Entry;
    013import java.util.Set;
    014import java.util.concurrent.ConcurrentMap;
    015import java.util.logging.Level;
    016
    017import com.google.common.collect.ArrayListMultimap;
    018import com.google.common.collect.ImmutableListMultimap;
    019import com.google.common.collect.ListMultimap;
    020import com.google.common.collect.Lists;
    021import com.google.common.collect.MapMaker;
    022import com.google.common.collect.Maps;
    023import com.google.common.collect.Sets;
    024
    025import cpw.mods.fml.common.FMLCommonHandler;
    026import cpw.mods.fml.common.FMLLog;
    027
    028import net.minecraft.nbt.NBTTagCompound;
    029import net.minecraft.server.MinecraftServer;
    030import net.minecraft.world.ChunkCoordIntPair;
    031import net.minecraft.world.MinecraftException;
    032import net.minecraft.world.World;
    033import net.minecraft.world.WorldManager;
    034import net.minecraft.world.WorldProvider;
    035import net.minecraft.world.WorldProviderEnd;
    036import net.minecraft.world.WorldProviderHell;
    037import net.minecraft.world.WorldProviderSurface;
    038import net.minecraft.world.WorldServer;
    039import net.minecraft.world.WorldServerMulti;
    040import net.minecraft.world.WorldSettings;
    041import net.minecraft.world.storage.ISaveHandler;
    042import net.minecraft.world.storage.SaveHandler;
    043import net.minecraftforge.event.world.WorldEvent;
    044
    045public class DimensionManager
    046{
    047    private static Hashtable<Integer, Class<? extends WorldProvider>> providers = new Hashtable<Integer, Class<? extends WorldProvider>>();
    048    private static Hashtable<Integer, Boolean> spawnSettings = new Hashtable<Integer, Boolean>();
    049    private static Hashtable<Integer, WorldServer> worlds = new Hashtable<Integer, WorldServer>();
    050    private static boolean hasInit = false;
    051    private static Hashtable<Integer, Integer> dimensions = new Hashtable<Integer, Integer>();
    052    private static ArrayList<Integer> unloadQueue = new ArrayList<Integer>();
    053    private static BitSet dimensionMap = new BitSet(Long.SIZE << 4);
    054    private static ConcurrentMap<World, World> weakWorldMap = new MapMaker().weakKeys().weakValues().<World,World>makeMap();
    055    private static Set<Integer> leakedWorlds = Sets.newHashSet();
    056
    057    public static boolean registerProviderType(int id, Class<? extends WorldProvider> provider, boolean keepLoaded)
    058    {
    059        if (providers.containsKey(id))
    060        {
    061            return false;
    062        }
    063        providers.put(id, provider);
    064        spawnSettings.put(id, keepLoaded);
    065        return true;
    066    }
    067
    068    /**
    069     * Unregisters a Provider type, and returns a array of all dimensions that are
    070     * registered to this provider type.
    071     * If the return size is greater then 0, it is required that the caller either
    072     * change those dimensions's registered type, or replace this type before the
    073     * world is attempted to load, else the loader will throw an exception.
    074     *
    075     * @param id The provider type ID to unreigster
    076     * @return An array containing all dimension IDs still registered to this provider type.
    077     */
    078    public static int[] unregisterProviderType(int id)
    079    {
    080        if (!providers.containsKey(id))
    081        {
    082            return new int[0];
    083        }
    084        providers.remove(id);
    085        spawnSettings.remove(id);
    086
    087        int[] ret = new int[dimensions.size()];
    088        int x = 0;
    089        for (Map.Entry<Integer, Integer> ent : dimensions.entrySet())
    090        {
    091            if (ent.getValue() == id)
    092            {
    093                ret[x++] = ent.getKey();
    094            }
    095        }
    096
    097        return Arrays.copyOf(ret, x);
    098    }
    099
    100    public static void init()
    101    {
    102        if (hasInit)
    103        {
    104            return;
    105        }
    106
    107        hasInit = true;
    108
    109        registerProviderType( 0, WorldProviderSurface.class, true);
    110        registerProviderType(-1, WorldProviderHell.class,    true);
    111        registerProviderType( 1, WorldProviderEnd.class,     false);
    112        registerDimension( 0,  0);
    113        registerDimension(-1, -1);
    114        registerDimension( 1,  1);
    115    }
    116
    117    public static void registerDimension(int id, int providerType)
    118    {
    119        if (!providers.containsKey(providerType))
    120        {
    121            throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, provider type %d does not exist", id, providerType));
    122        }
    123        if (dimensions.containsKey(id))
    124        {
    125            throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, One is already registered", id));
    126        }
    127        dimensions.put(id, providerType);
    128        if (id >= 0)
    129        {
    130            dimensionMap.set(id);
    131        }
    132    }
    133
    134    /**
    135     * For unregistering a dimension when the save is changed (disconnected from a server or loaded a new save
    136     */
    137    public static void unregisterDimension(int id)
    138    {
    139        if (!dimensions.containsKey(id))
    140        {
    141            throw new IllegalArgumentException(String.format("Failed to unregister dimension for id %d; No provider registered", id));
    142        }
    143        dimensions.remove(id);
    144    }
    145
    146    public static int getProviderType(int dim)
    147    {
    148        if (!dimensions.containsKey(dim))
    149        {
    150            throw new IllegalArgumentException(String.format("Could not get provider type for dimension %d, does not exist", dim));
    151        }
    152        return dimensions.get(dim);
    153    }
    154
    155    public static WorldProvider getProvider(int dim)
    156    {
    157        return getWorld(dim).provider;
    158    }
    159
    160    public static Integer[] getIDs(boolean check)
    161    {
    162        if (check)
    163        {
    164            List<World> allWorlds = Lists.newArrayList(weakWorldMap.keySet());
    165            allWorlds.removeAll(worlds.values());
    166            Set<Integer> newLeaks = Sets.newHashSet();
    167            for (ListIterator<World> li = allWorlds.listIterator(); li.hasNext(); )
    168            {
    169                World w = li.next();
    170                if (leakedWorlds.contains(System.identityHashCode(w)))
    171                {
    172                    li.remove();
    173                }
    174                newLeaks.add(System.identityHashCode(w));
    175            }
    176            leakedWorlds = newLeaks;
    177            if (allWorlds.size() > 0)
    178            {
    179                FMLLog.severe("Detected leaking worlds in memory. There are %d worlds that appear to be persisting. A mod is likely caching the world incorrectly\n", allWorlds.size() + leakedWorlds.size());
    180                for (World w : allWorlds)
    181                {
    182                    FMLLog.severe("The world %x (%s) has leaked.\n", System.identityHashCode(w), w.getWorldInfo().getWorldName());
    183                }
    184            }
    185        }
    186        return getIDs();
    187    }
    188    public static Integer[] getIDs()
    189    {
    190        return worlds.keySet().toArray(new Integer[worlds.size()]); //Only loaded dims, since usually used to cycle through loaded worlds
    191    }
    192
    193    public static void setWorld(int id, WorldServer world)
    194    {
    195        if (world != null) {
    196            worlds.put(id, world);
    197            weakWorldMap.put(world, world);
    198            MinecraftServer.getServer().worldTickTimes.put(id, new long[100]);
    199            FMLLog.info("Loading dimension %d (%s) (%s)", id, world.getWorldInfo().getWorldName(), world.getMinecraftServer());
    200        } else {
    201            worlds.remove(id);
    202            MinecraftServer.getServer().worldTickTimes.remove(id);
    203            FMLLog.info("Unloading dimension %d", id);
    204        }
    205
    206        ArrayList<WorldServer> tmp = new ArrayList<WorldServer>();
    207        if (worlds.get( 0) != null)
    208            tmp.add(worlds.get( 0));
    209        if (worlds.get(-1) != null)
    210            tmp.add(worlds.get(-1));
    211        if (worlds.get( 1) != null)
    212            tmp.add(worlds.get( 1));
    213
    214        for (Entry<Integer, WorldServer> entry : worlds.entrySet())
    215        {
    216            int dim = entry.getKey();
    217            if (dim >= -1 && dim <= 1)
    218            {
    219                continue;
    220            }
    221            tmp.add(entry.getValue());
    222        }
    223
    224        MinecraftServer.getServer().worldServers = tmp.toArray(new WorldServer[tmp.size()]);
    225    }
    226
    227    public static void initDimension(int dim) {
    228        WorldServer overworld = getWorld(0);
    229        if (overworld == null) {
    230            throw new RuntimeException("Cannot Hotload Dim: Overworld is not Loaded!");
    231        }
    232        try {
    233            DimensionManager.getProviderType(dim);
    234        } catch (Exception e) {
    235            System.err.println("Cannot Hotload Dim: " + e.getMessage());
    236            return; //If a provider hasn't been registered then we can't hotload the dim
    237        }
    238        MinecraftServer mcServer = overworld.getMinecraftServer();
    239        ISaveHandler savehandler = overworld.getSaveHandler();
    240        WorldSettings worldSettings = new WorldSettings(overworld.getWorldInfo());
    241
    242        WorldServer world = (dim == 0 ? overworld : new WorldServerMulti(mcServer, savehandler, overworld.getWorldInfo().getWorldName(), dim, worldSettings, overworld, mcServer.theProfiler));
    243        world.addWorldAccess(new WorldManager(mcServer, world));
    244        MinecraftForge.EVENT_BUS.post(new WorldEvent.Load(world));
    245        if (!mcServer.isSinglePlayer())
    246        {
    247            world.getWorldInfo().setGameType(mcServer.getGameType());
    248        }
    249
    250        mcServer.setDifficultyForAllWorlds(mcServer.getDifficulty());
    251    }
    252
    253    public static WorldServer getWorld(int id)
    254    {
    255        return worlds.get(id);
    256    }
    257
    258    public static WorldServer[] getWorlds()
    259    {
    260        return worlds.values().toArray(new WorldServer[worlds.size()]);
    261    }
    262
    263    public static boolean shouldLoadSpawn(int dim)
    264    {
    265        int id = getProviderType(dim);
    266        return spawnSettings.containsKey(id) && spawnSettings.get(id);
    267    }
    268
    269    static
    270    {
    271        init();
    272    }
    273
    274    /**
    275     * Not public API: used internally to get dimensions that should load at
    276     * server startup
    277     */
    278    public static Integer[] getStaticDimensionIDs()
    279    {
    280        return dimensions.keySet().toArray(new Integer[dimensions.keySet().size()]);
    281    }
    282    public static WorldProvider createProviderFor(int dim)
    283    {
    284        try
    285        {
    286            if (dimensions.containsKey(dim))
    287            {
    288                WorldProvider provider = providers.get(getProviderType(dim)).newInstance();
    289                provider.setDimension(dim);
    290                return provider;
    291            }
    292            else
    293            {
    294                throw new RuntimeException(String.format("No WorldProvider bound for dimension %d", dim)); //It's going to crash anyway at this point.  Might as well be informative
    295            }
    296        }
    297        catch (Exception e)
    298        {
    299            FMLCommonHandler.instance().getFMLLogger().log(Level.SEVERE,String.format("An error occured trying to create an instance of WorldProvider %d (%s)",
    300                    dim, providers.get(getProviderType(dim)).getSimpleName()),e);
    301            throw new RuntimeException(e);
    302        }
    303    }
    304
    305    public static void unloadWorld(int id) {
    306        unloadQueue.add(id);
    307    }
    308
    309    /*
    310    * To be called by the server at the appropriate time, do not call from mod code.
    311    */
    312    public static void unloadWorlds(Hashtable<Integer, long[]> worldTickTimes) {
    313        for (int id : unloadQueue) {
    314            WorldServer w = worlds.get(id);
    315            try {
    316                if (w != null)
    317                {
    318                    w.saveAllChunks(true, null);
    319                }
    320                else
    321                {
    322                    FMLLog.warning("Unexpected world unload - world %d is already unloaded", id);
    323                }
    324            } catch (MinecraftException e) {
    325                e.printStackTrace();
    326            }
    327            finally
    328            {
    329                if (w != null)
    330                {
    331                    MinecraftForge.EVENT_BUS.post(new WorldEvent.Unload(w));
    332                    w.flush();
    333                    setWorld(id, null);
    334                }
    335            }
    336        }
    337        unloadQueue.clear();
    338    }
    339
    340    /**
    341     * Return the next free dimension ID. Note: you are not guaranteed a contiguous
    342     * block of free ids. Always call for each individual ID you wish to get.
    343     * @return the next free dimension ID
    344     */
    345    public static int getNextFreeDimId() {
    346        int next = 0;
    347        while (true)
    348        {
    349            next = dimensionMap.nextClearBit(next);
    350            if (dimensions.containsKey(next))
    351            {
    352                dimensionMap.set(next);
    353            }
    354            else
    355            {
    356                return next;
    357            }
    358        }
    359    }
    360
    361    public static NBTTagCompound saveDimensionDataMap()
    362    {
    363        int[] data = new int[(dimensionMap.length() + Integer.SIZE - 1 )/ Integer.SIZE];
    364        NBTTagCompound dimMap = new NBTTagCompound();
    365        for (int i = 0; i < data.length; i++)
    366        {
    367            int val = 0;
    368            for (int j = 0; j < Integer.SIZE; j++)
    369            {
    370                val |= dimensionMap.get(i * Integer.SIZE + j) ? (1 << j) : 0;
    371            }
    372            data[i] = val;
    373        }
    374        dimMap.setIntArray("DimensionArray", data);
    375        return dimMap;
    376    }
    377
    378    public static void loadDimensionDataMap(NBTTagCompound compoundTag)
    379    {
    380        if (compoundTag == null)
    381        {
    382            dimensionMap.clear();
    383            for (Integer id : dimensions.keySet())
    384            {
    385                if (id >= 0)
    386                {
    387                    dimensionMap.set(id);
    388                }
    389            }
    390        }
    391        else
    392        {
    393            int[] intArray = compoundTag.getIntArray("DimensionArray");
    394            for (int i = 0; i < intArray.length; i++)
    395            {
    396                for (int j = 0; j < Integer.SIZE; j++)
    397                {
    398                    dimensionMap.set(i * Integer.SIZE + j, (intArray[i] & (1 << j)) != 0);
    399                }
    400            }
    401        }
    402    }
    403
    404    /**
    405     * Return the current root directory for the world save. Accesses getSaveHandler from the overworld
    406     * @return the root directory of the save
    407     */
    408    public static File getCurrentSaveRootDirectory()
    409    {
    410        if (DimensionManager.getWorld(0) != null)
    411        {
    412            return ((SaveHandler)DimensionManager.getWorld(0).getSaveHandler()).getSaveDirectory();
    413        }
    414        else if (MinecraftServer.getServer() != null)
    415        {
    416            MinecraftServer srv = MinecraftServer.getServer();
    417            SaveHandler saveHandler = (SaveHandler) srv.getActiveAnvilConverter().getSaveLoader(srv.getFolderName(), false);
    418            return saveHandler.getSaveDirectory();
    419        }
    420        else
    421        {
    422            return null;
    423        }
    424    }
    425}