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, 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()).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}