001    package net.minecraftforge.common;
002    
003    import java.io.DataInputStream;
004    import java.io.File;
005    import java.io.FileInputStream;
006    import java.io.IOException;
007    import java.util.HashSet;
008    import java.util.LinkedHashSet;
009    import java.util.LinkedList;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.Set;
013    import java.util.UUID;
014    import java.util.logging.Level;
015    
016    import com.google.common.cache.Cache;
017    import com.google.common.cache.CacheBuilder;
018    import com.google.common.collect.ArrayListMultimap;
019    import com.google.common.collect.BiMap;
020    import com.google.common.collect.HashBiMap;
021    import com.google.common.collect.HashMultimap;
022    import com.google.common.collect.ImmutableList;
023    import com.google.common.collect.ImmutableSet;
024    import com.google.common.collect.ImmutableSetMultimap;
025    import com.google.common.collect.LinkedHashMultimap;
026    import com.google.common.collect.ListMultimap;
027    import com.google.common.collect.Lists;
028    import com.google.common.collect.MapMaker;
029    import com.google.common.collect.Maps;
030    import com.google.common.collect.Multimap;
031    import com.google.common.collect.Multiset;
032    import com.google.common.collect.SetMultimap;
033    import com.google.common.collect.Sets;
034    import com.google.common.collect.TreeMultiset;
035    
036    import cpw.mods.fml.common.FMLLog;
037    import cpw.mods.fml.common.Loader;
038    import cpw.mods.fml.common.ModContainer;
039    
040    import net.minecraft.src.Chunk;
041    import net.minecraft.src.ChunkCoordIntPair;
042    import net.minecraft.src.CompressedStreamTools;
043    import net.minecraft.src.Entity;
044    import net.minecraft.src.EntityPlayer;
045    import net.minecraft.src.MathHelper;
046    import net.minecraft.src.NBTBase;
047    import net.minecraft.src.NBTTagCompound;
048    import net.minecraft.src.NBTTagList;
049    import net.minecraft.src.World;
050    import net.minecraft.src.WorldServer;
051    import net.minecraftforge.common.ForgeChunkManager.Ticket;
052    
053    /**
054     * Manages chunkloading for mods.
055     *
056     * The basic principle is a ticket based system.
057     * 1. Mods register a callback {@link #setForcedChunkLoadingCallback(Object, LoadingCallback)}
058     * 2. Mods ask for a ticket {@link #requestTicket(Object, World, Type)} and then hold on to that ticket.
059     * 3. Mods request chunks to stay loaded {@link #forceChunk(Ticket, ChunkCoordIntPair)} or remove chunks from force loading {@link #unforceChunk(Ticket, ChunkCoordIntPair)}.
060     * 4. When a world unloads, the tickets associated with that world are saved by the chunk manager.
061     * 5. When a world loads, saved tickets are offered to the mods associated with the tickets. The {@link Ticket#getModData()} that is set by the mod should be used to re-register
062     * chunks to stay loaded (and maybe take other actions).
063     *
064     * The chunkloading is configurable at runtime. The file "config/forgeChunkLoading.cfg" contains both default configuration for chunkloading, and a sample individual mod
065     * specific override section.
066     *
067     * @author cpw
068     *
069     */
070    public class ForgeChunkManager
071    {
072        private static int defaultMaxCount;
073        private static int defaultMaxChunks;
074        private static boolean overridesEnabled;
075    
076        private static Map<World, Multimap<String, Ticket>> tickets = new MapMaker().weakKeys().makeMap();
077        private static Map<String, Integer> ticketConstraints = Maps.newHashMap();
078        private static Map<String, Integer> chunkConstraints = Maps.newHashMap();
079    
080        private static SetMultimap<String, Ticket> playerTickets = HashMultimap.create();
081    
082        private static Map<String, LoadingCallback> callbacks = Maps.newHashMap();
083    
084        private static Map<World, ImmutableSetMultimap<ChunkCoordIntPair,Ticket>> forcedChunks = new MapMaker().weakKeys().makeMap();
085        private static BiMap<UUID,Ticket> pendingEntities = HashBiMap.create();
086    
087        private static Map<World,Cache<Long, Chunk>> dormantChunkCache = new MapMaker().weakKeys().makeMap();
088    
089        private static File cfgFile;
090        private static Configuration config;
091        private static int playerTicketLength;
092        private static int dormantChunkCacheSize;
093        /**
094         * All mods requiring chunkloading need to implement this to handle the
095         * re-registration of chunk tickets at world loading time
096         *
097         * @author cpw
098         *
099         */
100        public interface LoadingCallback
101        {
102            /**
103             * Called back when tickets are loaded from the world to allow the
104             * mod to re-register the chunks associated with those tickets. The list supplied
105             * here is truncated to length prior to use. Tickets unwanted by the
106             * mod must be disposed of manually unless the mod is an OrderedLoadingCallback instance
107             * in which case, they will have been disposed of by the earlier callback.
108             *
109             * @param tickets The tickets to re-register. The list is immutable and cannot be manipulated directly. Copy it first.
110             * @param world the world
111             */
112            public void ticketsLoaded(List<Ticket> tickets, World world);
113        }
114    
115        /**
116         * This is a special LoadingCallback that can be implemented as well as the
117         * LoadingCallback to provide access to additional behaviour.
118         * Specifically, this callback will fire prior to Forge dropping excess
119         * tickets. Tickets in the returned list are presumed ordered and excess will
120         * be truncated from the returned list.
121         * This allows the mod to control not only if they actually <em>want</em> a ticket but
122         * also their preferred ticket ordering.
123         *
124         * @author cpw
125         *
126         */
127        public interface OrderedLoadingCallback extends LoadingCallback
128        {
129            /**
130             * Called back when tickets are loaded from the world to allow the
131             * mod to decide if it wants the ticket still, and prioritise overflow
132             * based on the ticket count.
133             * WARNING: You cannot force chunks in this callback, it is strictly for allowing the mod
134             * to be more selective in which tickets it wishes to preserve in an overflow situation
135             *
136             * @param tickets The tickets that you will want to select from. The list is immutable and cannot be manipulated directly. Copy it first.
137             * @param world The world
138             * @param maxTicketCount The maximum number of tickets that will be allowed.
139             * @return A list of the tickets this mod wishes to continue using. This list will be truncated
140             * to "maxTicketCount" size after the call returns and then offered to the other callback
141             * method
142             */
143            public List<Ticket> ticketsLoaded(List<Ticket> tickets, World world, int maxTicketCount);
144        }
145        public enum Type
146        {
147    
148            /**
149             * For non-entity registrations
150             */
151            NORMAL,
152            /**
153             * For entity registrations
154             */
155            ENTITY
156        }
157        public static class Ticket
158        {
159            private String modId;
160            private Type ticketType;
161            private LinkedHashSet<ChunkCoordIntPair> requestedChunks;
162            private NBTTagCompound modData;
163            private World world;
164            private int maxDepth;
165            private String entityClazz;
166            private int entityChunkX;
167            private int entityChunkZ;
168            private Entity entity;
169            private String player;
170    
171            Ticket(String modId, Type type, World world)
172            {
173                this.modId = modId;
174                this.ticketType = type;
175                this.world = world;
176                this.maxDepth = getMaxChunkDepthFor(modId);
177                this.requestedChunks = Sets.newLinkedHashSet();
178            }
179    
180            Ticket(String modId, Type type, World world, EntityPlayer player)
181            {
182                this(modId, type, world);
183                if (player != null)
184                {
185                    this.player = player.getEntityName();
186                }
187                else
188                {
189                    FMLLog.log(Level.SEVERE, "Attempt to create a player ticket without a valid player");
190                    throw new RuntimeException();
191                }
192            }
193            /**
194             * The chunk list depth can be manipulated up to the maximal grant allowed for the mod. This value is configurable. Once the maximum is reached,
195             * the least recently forced chunk, by original registration time, is removed from the forced chunk list.
196             *
197             * @param depth The new depth to set
198             */
199            public void setChunkListDepth(int depth)
200            {
201                if (depth > getMaxChunkDepthFor(modId) || (depth <= 0 && getMaxChunkDepthFor(modId) > 0))
202                {
203                    FMLLog.warning("The mod %s tried to modify the chunk ticket depth to: %d, its allowed maximum is: %d", modId, depth, getMaxChunkDepthFor(modId));
204                }
205                else
206                {
207                    this.maxDepth = depth;
208                }
209            }
210    
211            /**
212             * Gets the current max depth for this ticket.
213             * Should be the same as getMaxChunkListDepth()
214             * unless setChunkListDepth has been called.
215             *
216             * @return Current max depth
217             */
218            public int getChunkListDepth()
219            {
220                return maxDepth;
221            }
222    
223            /**
224             * Get the maximum chunk depth size
225             *
226             * @return The maximum chunk depth size
227             */
228            public int getMaxChunkListDepth()
229            {
230                return getMaxChunkDepthFor(modId);
231            }
232    
233            /**
234             * Bind the entity to the ticket for {@link Type#ENTITY} type tickets. Other types will throw a runtime exception.
235             *
236             * @param entity The entity to bind
237             */
238            public void bindEntity(Entity entity)
239            {
240                if (ticketType!=Type.ENTITY)
241                {
242                    throw new RuntimeException("Cannot bind an entity to a non-entity ticket");
243                }
244                this.entity = entity;
245            }
246    
247            /**
248             * Retrieve the {@link NBTTagCompound} that stores mod specific data for the chunk ticket.
249             * Example data to store would be a TileEntity or Block location. This is persisted with the ticket and
250             * provided to the {@link LoadingCallback} for the mod. It is recommended to use this to recover
251             * useful state information for the forced chunks.
252             *
253             * @return The custom compound tag for mods to store additional chunkloading data
254             */
255            public NBTTagCompound getModData()
256            {
257                if (this.modData == null)
258                {
259                    this.modData = new NBTTagCompound();
260                }
261                return modData;
262            }
263    
264            /**
265             * Get the entity associated with this {@link Type#ENTITY} type ticket
266             * @return
267             */
268            public Entity getEntity()
269            {
270                return entity;
271            }
272    
273            /**
274             * Is this a player associated ticket rather than a mod associated ticket?
275             */
276            public boolean isPlayerTicket()
277            {
278                return player != null;
279            }
280    
281            /**
282             * Get the player associated with this ticket
283             */
284            public String getPlayerName()
285            {
286                return player;
287            }
288    
289            /**
290             * Get the associated mod id
291             */
292            public String getModId()
293            {
294                return modId;
295            }
296    
297            /**
298             * Gets the ticket type
299             */
300            public Type getType()
301            {
302                return ticketType;
303            }
304    
305            /**
306             * Gets a list of requested chunks for this ticket.
307             */
308            public ImmutableSet getChunkList()
309            {
310                return ImmutableSet.copyOf(requestedChunks);
311            }
312        }
313    
314        static void loadWorld(World world)
315        {
316            ArrayListMultimap<String, Ticket> newTickets = ArrayListMultimap.<String, Ticket>create();
317            tickets.put(world, newTickets);
318    
319            forcedChunks.put(world, ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of());
320    
321            if (!(world instanceof WorldServer))
322            {
323                return;
324            }
325    
326            dormantChunkCache.put(world, CacheBuilder.newBuilder().maximumSize(dormantChunkCacheSize).<Long, Chunk>build());
327            WorldServer worldServer = (WorldServer) world;
328            File chunkDir = worldServer.getChunkSaveLocation();
329            File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
330    
331            if (chunkLoaderData.exists() && chunkLoaderData.isFile())
332            {
333                ArrayListMultimap<String, Ticket> loadedTickets = ArrayListMultimap.<String, Ticket>create();
334                ArrayListMultimap<String, Ticket> playerLoadedTickets = ArrayListMultimap.<String, Ticket>create();
335                NBTTagCompound forcedChunkData;
336                try
337                {
338                    forcedChunkData = CompressedStreamTools.read(chunkLoaderData);
339                }
340                catch (IOException e)
341                {
342                    FMLLog.log(Level.WARNING, e, "Unable to read forced chunk data at %s - it will be ignored", chunkLoaderData.getAbsolutePath());
343                    return;
344                }
345                NBTTagList ticketList = forcedChunkData.getTagList("TicketList");
346                for (int i = 0; i < ticketList.tagCount(); i++)
347                {
348                    NBTTagCompound ticketHolder = (NBTTagCompound) ticketList.tagAt(i);
349                    String modId = ticketHolder.getString("Owner");
350                    boolean isPlayer = "Forge".equals(modId);
351    
352                    if (!isPlayer && !Loader.isModLoaded(modId))
353                    {
354                        FMLLog.warning("Found chunkloading data for mod %s which is currently not available or active - it will be removed from the world save", modId);
355                        continue;
356                    }
357    
358                    if (!isPlayer && !callbacks.containsKey(modId))
359                    {
360                        FMLLog.warning("The mod %s has registered persistent chunkloading data but doesn't seem to want to be called back with it - it will be removed from the world save", modId);
361                        continue;
362                    }
363    
364                    NBTTagList tickets = ticketHolder.getTagList("Tickets");
365                    for (int j = 0; j < tickets.tagCount(); j++)
366                    {
367                        NBTTagCompound ticket = (NBTTagCompound) tickets.tagAt(j);
368                        modId = ticket.hasKey("ModId") ? ticket.getString("ModId") : modId;
369                        Type type = Type.values()[ticket.getByte("Type")];
370                        byte ticketChunkDepth = ticket.getByte("ChunkListDepth");
371                        Ticket tick = new Ticket(modId, type, world);
372                        if (ticket.hasKey("ModData"))
373                        {
374                            tick.modData = ticket.getCompoundTag("ModData");
375                        }
376                        if (ticket.hasKey("Player"))
377                        {
378                            tick.player = ticket.getString("Player");
379                            playerLoadedTickets.put(tick.modId, tick);
380                            playerTickets.put(tick.player, tick);
381                        }
382                        else
383                        {
384                            loadedTickets.put(modId, tick);
385                        }
386                        if (type == Type.ENTITY)
387                        {
388                            tick.entityChunkX = ticket.getInteger("chunkX");
389                            tick.entityChunkZ = ticket.getInteger("chunkZ");
390                            UUID uuid = new UUID(ticket.getLong("PersistentIDMSB"), ticket.getLong("PersistentIDLSB"));
391                            // add the ticket to the "pending entity" list
392                            pendingEntities.put(uuid, tick);
393                        }
394                    }
395                }
396    
397                for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
398                {
399                    if (tick.ticketType == Type.ENTITY && tick.entity == null)
400                    {
401                        // force the world to load the entity's chunk
402                        // the load will come back through the loadEntity method and attach the entity
403                        // to the ticket
404                        world.getChunkFromChunkCoords(tick.entityChunkX, tick.entityChunkZ);
405                    }
406                }
407                for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
408                {
409                    if (tick.ticketType == Type.ENTITY && tick.entity == null)
410                    {
411                        FMLLog.warning("Failed to load persistent chunkloading entity %s from store.", pendingEntities.inverse().get(tick));
412                        loadedTickets.remove(tick.modId, tick);
413                    }
414                }
415                pendingEntities.clear();
416                // send callbacks
417                for (String modId : loadedTickets.keySet())
418                {
419                    LoadingCallback loadingCallback = callbacks.get(modId);
420                    int maxTicketLength = getMaxTicketLengthFor(modId);
421                    List<Ticket> tickets = loadedTickets.get(modId);
422                    if (loadingCallback instanceof OrderedLoadingCallback)
423                    {
424                        OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback;
425                        tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength);
426                    }
427                    if (tickets.size() > maxTicketLength)
428                    {
429                        FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size());
430                        tickets.subList(maxTicketLength, tickets.size()).clear();
431                    }
432                    ForgeChunkManager.tickets.get(world).putAll(modId, tickets);
433                    loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
434                }
435                for (String modId : playerLoadedTickets.keySet())
436                {
437                    LoadingCallback loadingCallback = callbacks.get(modId);
438                    List<Ticket> tickets = playerLoadedTickets.get(modId);
439                    ForgeChunkManager.tickets.get(world).putAll("Forge", tickets);
440                    loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
441                }
442            }
443        }
444    
445        /**
446         * Set a chunkloading callback for the supplied mod object
447         *
448         * @param mod  The mod instance registering the callback
449         * @param callback The code to call back when forced chunks are loaded
450         */
451        public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback)
452        {
453            ModContainer container = getContainer(mod);
454            if (container == null)
455            {
456                FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
457                return;
458            }
459    
460            callbacks.put(container.getModId(), callback);
461        }
462    
463        /**
464         * Discover the available tickets for the mod in the world
465         *
466         * @param mod The mod that will own the tickets
467         * @param world The world
468         * @return The count of tickets left for the mod in the supplied world
469         */
470        public static int ticketCountAvailableFor(Object mod, World world)
471        {
472            ModContainer container = getContainer(mod);
473            if (container!=null)
474            {
475                String modId = container.getModId();
476                int allowedCount = getMaxTicketLengthFor(modId);
477                return allowedCount - tickets.get(world).get(modId).size();
478            }
479            else
480            {
481                return 0;
482            }
483        }
484    
485        private static ModContainer getContainer(Object mod)
486        {
487            ModContainer container = Loader.instance().getModObjectList().inverse().get(mod);
488            return container;
489        }
490    
491        private static int getMaxTicketLengthFor(String modId)
492        {
493            int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount;
494            return allowedCount;
495        }
496    
497        private static int getMaxChunkDepthFor(String modId)
498        {
499            int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks;
500            return allowedCount;
501        }
502    
503        public static Ticket requestPlayerTicket(Object mod, EntityPlayer player, World world, Type type)
504        {
505            ModContainer mc = getContainer(mod);
506            if (mc == null)
507            {
508                FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
509                return null;
510            }
511            if (playerTickets.get(player.getEntityName()).size()>playerTicketLength)
512            {
513                FMLLog.warning("Unable to assign further chunkloading tickets to player %s (on behalf of mod %s)", player.getEntityName(), mc.getModId());
514                return null;
515            }
516            Ticket ticket = new Ticket(mc.getModId(),type,world,player);
517            playerTickets.put(player.getEntityName(), ticket);
518            tickets.get(world).put("Forge", ticket);
519            return ticket;
520        }
521        /**
522         * Request a chunkloading ticket of the appropriate type for the supplied mod
523         *
524         * @param mod The mod requesting a ticket
525         * @param world The world in which it is requesting the ticket
526         * @param type The type of ticket
527         * @return A ticket with which to register chunks for loading, or null if no further tickets are available
528         */
529        public static Ticket requestTicket(Object mod, World world, Type type)
530        {
531            ModContainer container = getContainer(mod);
532            if (container == null)
533            {
534                FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
535                return null;
536            }
537            String modId = container.getModId();
538            if (!callbacks.containsKey(modId))
539            {
540                FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId);
541                throw new RuntimeException("Invalid ticket request");
542            }
543    
544            int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount;
545    
546            if (tickets.get(world).get(modId).size() >= allowedCount)
547            {
548                FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
549                return null;
550            }
551            Ticket ticket = new Ticket(modId, type, world);
552            tickets.get(world).put(modId, ticket);
553    
554            return ticket;
555        }
556    
557        /**
558         * Release the ticket back to the system. This will also unforce any chunks held by the ticket so that they can be unloaded and/or stop ticking.
559         *
560         * @param ticket The ticket to release
561         */
562        public static void releaseTicket(Ticket ticket)
563        {
564            if (ticket == null)
565            {
566                return;
567            }
568            if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
569            {
570                return;
571            }
572            if (ticket.requestedChunks!=null)
573            {
574                for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
575                {
576                    unforceChunk(ticket, chunk);
577                }
578            }
579            if (ticket.isPlayerTicket())
580            {
581                playerTickets.remove(ticket.player, ticket);
582                tickets.get(ticket.world).remove("Forge",ticket);
583            }
584            else
585            {
586                tickets.get(ticket.world).remove(ticket.modId, ticket);
587            }
588        }
589    
590        /**
591         * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
592         * recently registered chunk is unforced and may be unloaded.
593         * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
594         *
595         * @param ticket The ticket registering the chunk
596         * @param chunk The chunk to force
597         */
598        public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
599        {
600            if (ticket == null || chunk == null)
601            {
602                return;
603            }
604            if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
605            {
606                throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
607            }
608            if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
609            {
610                FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
611                return;
612            }
613            ticket.requestedChunks.add(chunk);
614            ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build();
615            forcedChunks.put(ticket.world, newMap);
616            if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
617            {
618                ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
619                unforceChunk(ticket,removed);
620            }
621        }
622    
623        /**
624         * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
625         * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
626         * in the ticket list
627         *
628         * @param ticket The ticket holding the chunk list
629         * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
630         */
631        public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
632        {
633            if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
634            {
635                return;
636            }
637            ticket.requestedChunks.remove(chunk);
638            ticket.requestedChunks.add(chunk);
639        }
640        /**
641         * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
642         *
643         * @param ticket The ticket holding the chunk
644         * @param chunk The chunk to unforce
645         */
646        public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
647        {
648            if (ticket == null || chunk == null)
649            {
650                return;
651            }
652            ticket.requestedChunks.remove(chunk);
653            LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world));
654            copy.remove(chunk, ticket);
655            ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy);
656            forcedChunks.put(ticket.world,newMap);
657        }
658    
659        static void loadConfiguration()
660        {
661            for (String mod : config.categories.keySet())
662            {
663                if (mod.equals("Forge") || mod.equals("defaults"))
664                {
665                    continue;
666                }
667                Property modTC = config.get(mod, "maximumTicketCount", 200);
668                Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
669                ticketConstraints.put(mod, modTC.getInt(200));
670                chunkConstraints.put(mod, modCPT.getInt(25));
671            }
672            config.save();
673        }
674    
675        /**
676         * The list of persistent chunks in the world. This set is immutable.
677         * @param world
678         * @return
679         */
680        public static SetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
681        {
682            return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
683        }
684    
685        static void saveWorld(World world)
686        {
687            // only persist persistent worlds
688            if (!(world instanceof WorldServer)) { return; }
689            WorldServer worldServer = (WorldServer) world;
690            File chunkDir = worldServer.getChunkSaveLocation();
691            File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
692    
693            NBTTagCompound forcedChunkData = new NBTTagCompound();
694            NBTTagList ticketList = new NBTTagList();
695            forcedChunkData.setTag("TicketList", ticketList);
696    
697            Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
698            for (String modId : ticketSet.keySet())
699            {
700                NBTTagCompound ticketHolder = new NBTTagCompound();
701                ticketList.appendTag(ticketHolder);
702    
703                ticketHolder.setString("Owner", modId);
704                NBTTagList tickets = new NBTTagList();
705                ticketHolder.setTag("Tickets", tickets);
706    
707                for (Ticket tick : ticketSet.get(modId))
708                {
709                    NBTTagCompound ticket = new NBTTagCompound();
710                    ticket.setByte("Type", (byte) tick.ticketType.ordinal());
711                    ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
712                    if (tick.isPlayerTicket())
713                    {
714                        ticket.setString("ModId", tick.modId);
715                        ticket.setString("Player", tick.player);
716                    }
717                    if (tick.modData != null)
718                    {
719                        ticket.setCompoundTag("ModData", tick.modData);
720                    }
721                    if (tick.ticketType == Type.ENTITY && tick.entity != null)
722                    {
723                        ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
724                        ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
725                        ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
726                        ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
727                        tickets.appendTag(ticket);
728                    }
729                    else if (tick.ticketType != Type.ENTITY)
730                    {
731                        tickets.appendTag(ticket);
732                    }
733                }
734            }
735            try
736            {
737                CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
738            }
739            catch (IOException e)
740            {
741                FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
742                return;
743            }
744        }
745    
746        static void loadEntity(Entity entity)
747        {
748            UUID id = entity.getPersistentID();
749            Ticket tick = pendingEntities.get(id);
750            if (tick != null)
751            {
752                tick.bindEntity(entity);
753                pendingEntities.remove(id);
754            }
755        }
756    
757        public static void putDormantChunk(long coords, Chunk chunk)
758        {
759            Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
760            if (cache != null)
761            {
762                cache.put(coords, chunk);
763            }
764        }
765    
766        public static Chunk fetchDormantChunk(long coords, World world)
767        {
768            Cache<Long, Chunk> cache = dormantChunkCache.get(world);
769            return cache == null ? null : cache.getIfPresent(coords);
770        }
771    
772        static void captureConfig(File configDir)
773        {
774            cfgFile = new File(configDir,"forgeChunkLoading.cfg");
775            config = new Configuration(cfgFile, true);
776            config.categories.clear();
777            try
778            {
779                config.load();
780            }
781            catch (Exception e)
782            {
783                File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
784                if (dest.exists())
785                {
786                    dest.delete();
787                }
788                cfgFile.renameTo(dest);
789                FMLLog.log(Level.SEVERE, e, "A critical error occured reading the forgeChunkLoading.cfg file, defaults will be used - the invalid file is backed up at forgeChunkLoading.cfg.bak");
790            }
791            config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
792            Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
793            maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
794                        "in this file. This is the number of chunk loading requests a mod is allowed to make.";
795            defaultMaxCount = maxTicketCount.getInt(200);
796    
797            Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
798            maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
799                        "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
800            defaultMaxChunks = maxChunks.getInt(25);
801    
802            Property playerTicketCount = config.get("defaults", "playetTicketCount", 500);
803            playerTicketCount.comment = "The number of tickets a player can be assigned instead of a mod. This is shared across all mods and it is up to the mods to use it.";
804            playerTicketLength = playerTicketCount.getInt(500);
805    
806            Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
807            dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
808                        "loading times. Specify the size of that cache here";
809            dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
810            FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
811    
812            Property modOverridesEnabled = config.get("defaults", "enabled", true);
813            modOverridesEnabled.comment = "Are mod overrides enabled?";
814            overridesEnabled = modOverridesEnabled.getBoolean(true);
815    
816            config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
817                    "Copy this section and rename the with the modid for the mod you wish to override.\n" +
818                    "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
819                    "for that mod");
820    
821            Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
822            sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
823            sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
824            sampleTC.comment = "Maximum chunks per ticket for the mod.";
825            for (String mod : config.categories.keySet())
826            {
827                if (mod.equals("Forge") || mod.equals("defaults"))
828                {
829                    continue;
830                }
831                Property modTC = config.get(mod, "maximumTicketCount", 200);
832                Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
833            }
834        }
835    
836    
837        public static Map<String,Property> getConfigMapFor(Object mod)
838        {
839            ModContainer container = getContainer(mod);
840            if (container != null)
841            {
842                Map<String, Property> map = config.categories.get(container.getModId());
843                if (map == null)
844                {
845                    map = Maps.newHashMap();
846                    config.categories.put(container.getModId(), map);
847                }
848                return map;
849            }
850    
851            return null;
852        }
853    
854        public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
855        {
856            ModContainer container = getContainer(mod);
857            if (container != null)
858            {
859                Map<String, Property> props = config.categories.get(container.getModId());
860                props.put(propertyName, new Property(propertyName, value, type));
861            }
862        }
863    }