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