    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.HashMultiset;
    019import com.google.common.collect.ImmutableListMultimap;
    020import com.google.common.collect.ListMultimap;
    021import com.google.common.collect.Lists;
    022import com.google.common.collect.MapMaker;
    023import com.google.common.collect.Maps;
    024import com.google.common.collect.Multiset;
    025import com.google.common.collect.Sets;
    026
    027import cpw.mods.fml.common.FMLCommonHandler;
    028import cpw.mods.fml.common.FMLLog;
    029
    030import net.minecraft.nbt.NBTTagCompound;
    031import net.minecraft.server.MinecraftServer;
    032import net.minecraft.world.ChunkCoordIntPair;
    033import net.minecraft.world.MinecraftException;
    034import net.minecraft.world.World;
    035import net.minecraft.world.WorldManager;
    036import net.minecraft.world.WorldProvider;
    037import net.minecraft.world.WorldProviderEnd;
    038import net.minecraft.world.WorldProviderHell;
    039import net.minecraft.world.WorldProviderSurface;
    040import net.minecraft.world.WorldServer;
    041import net.minecraft.world.WorldServerMulti;
    042import net.minecraft.world.WorldSettings;
    043import net.minecraft.world.storage.ISaveHandler;
    044import net.minecraft.world.storage.SaveHandler;
    045import net.minecraftforge.event.world.WorldEvent;
    046
    047public class DimensionManager
    048{
    049    private static Hashtable<Integer, Class<? extends WorldProvider>> providers = new Hashtable<Integer, Class<? extends WorldProvider>>();
    050    private static Hashtable<Integer, Boolean> spawnSettings = new Hashtable<Integer, Boolean>();
    051    private static Hashtable<Integer, WorldServer> worlds = new Hashtable<Integer, WorldServer>();
    052    private static boolean hasInit = false;
    053    private static Hashtable<Integer, Integer> dimensions = new Hashtable<Integer, Integer>();
    054    private static ArrayList<Integer> unloadQueue = new ArrayList<Integer>();
    055    private static BitSet dimensionMap = new BitSet(Long.SIZE << 4);
    056    private static ConcurrentMap<World, World> weakWorldMap = new MapMaker().weakKeys().weakValues().<World,World>makeMap();
    057    private static Multiset<Integer> leakedWorlds = HashMultiset.create();
    058
    059    public static boolean registerProviderType(int id, Class<? extends WorldProvider> provider, boolean keepLoaded)
    060    {
    061        if (providers.containsKey(id))
    062        {
    063            return false;
    064        }
    065        providers.put(id, provider);
    066        spawnSettings.put(id, keepLoaded);
    067        return true;
    068    }
    069
    070    /**
    071     * Unregisters a Provider type, and returns a array of all dimensions that are
    072     * registered to this provider type.
    073     * If the return size is greater then 0, it is required that the caller either
    074     * change those dimensions's registered type, or replace this type before the
    075     * world is attempted to load, else the loader will throw an exception.
    076     *
    077     * @param id The provider type ID to unreigster
    078     * @return An array containing all dimension IDs still registered to this provider type.
    079     */
    080    public static int[] unregisterProviderType(int id)
    081    {
    082        if (!providers.containsKey(id))
    083        {
    084            return new int[0];
    085        }
    086        providers.remove(id);
    087        spawnSettings.remove(id);
    088
    089        int[] ret = new int[dimensions.size()];
    090        int x = 0;
    091        for (Map.Entry<Integer, Integer> ent : dimensions.entrySet())
    092        {
    093            if (ent.getValue() == id)
    094            {
    095                ret[x++] = ent.getKey();
    096            }
    097        }
    098
    099        return Arrays.copyOf(ret, x);
    100    }
    101
    102    public static void init()
    103    {
    104        if (hasInit)
    105        {
    106            return;
    107        }
    108
    109        hasInit = true;
    110
    111        registerProviderType( 0, WorldProviderSurface.class, true);
    112        registerProviderType(-1, WorldProviderHell.class,    true);
    113        registerProviderType( 1, WorldProviderEnd.class,     false);
    114        registerDimension( 0,  0);
    115        registerDimension(-1, -1);
    116        registerDimension( 1,  1);
    117    }
    118
    119    public static void registerDimension(int id, int providerType)
    120    {
    121        if (!providers.containsKey(providerType))
    122        {
    123            throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, provider type %d does not exist", id, providerType));
    124        }
    125        if (dimensions.containsKey(id))
    126        {
    127            throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, One is already registered", id));
    128        }
    129        dimensions.put(id, providerType);
    130        if (id >= 0)
    131        {
    132            dimensionMap.set(id);
    133        }
    134    }
    135
    136    /**
    137     * For unregistering a dimension when the save is changed (disconnected from a server or loaded a new save
    138     */
    139    public static void unregisterDimension(int id)
    140    {
    141        if (!dimensions.containsKey(id))
    142        {
    143            throw new IllegalArgumentException(String.format("Failed to unregister dimension for id %d; No provider registered", id));
    144        }
    145        dimensions.remove(id);
    146    }
    147
    148    public static int getProviderType(int dim)
    149    {
    150        if (!dimensions.containsKey(dim))
    151        {
    152            throw new IllegalArgumentException(String.format("Could not get provider type for dimension %d, does not exist", dim));
    153        }
    154        return dimensions.get(dim);
    155    }
    156
    157    public static WorldProvider getProvider(int dim)
    158    {
    159        return getWorld(dim).provider;
    160    }
    161
    162    public static Integer[] getIDs(boolean check)
    163    {
    164        if (check)
    165        {
    166            List<World> allWorlds = Lists.newArrayList(weakWorldMap.keySet());
    167            allWorlds.removeAll(worlds.values());
    168            for (ListIterator<World> li = allWorlds.listIterator(); li.hasNext(); )
    169            {
    170                World w = li.next();
    171                leakedWorlds.add(System.identityHashCode(w));
    172            }
    173            for (World w : allWorlds)
    174            {
    175                int leakCount = leakedWorlds.count(System.identityHashCode(w));
    176                if (leakCount == 5)
    177                {
    178                    FMLLog.fine("The world %x (%s) may have leaked: first encounter (5 occurences).\n", System.identityHashCode(w), w.getWorldInfo().getWorldName());
    179                }
    180                else if (leakCount % 5 == 0)
    181                {
    182                    FMLLog.fine("The world %x (%s) may have leaked: seen %d times.\n", System.identityHashCode(w), w.getWorldInfo().getWorldName(), leakCount);
    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, overworld.getWorldLogAgent()));
    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()).getWorldDirectory();
    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.getWorldDirectory();
    419        }
    420        else
    421        {
    422            return null;
    423        }
    424    }
    425}