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