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