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> loadedTickets = ArrayListMultimap.<String, Ticket>create();
240            tickets.put(world, loadedTickets);
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                NBTTagCompound forcedChunkData;
257                try
258                {
259                    forcedChunkData = CompressedStreamTools.read(chunkLoaderData);
260                }
261                catch (IOException e)
262                {
263                    FMLLog.log(Level.WARNING, e, "Unable to read forced chunk data at %s - it will be ignored", chunkLoaderData.getAbsolutePath());
264                    return;
265                }
266                NBTTagList ticketList = forcedChunkData.getTagList("TicketList");
267                for (int i = 0; i < ticketList.tagCount(); i++)
268                {
269                    NBTTagCompound ticketHolder = (NBTTagCompound) ticketList.tagAt(i);
270                    String modId = ticketHolder.getString("Owner");
271    
272                    if (!Loader.isModLoaded(modId))
273                    {
274                        FMLLog.warning("Found chunkloading data for mod %s which is currently not available or active - it will be removed from the world save", modId);
275                        continue;
276                    }
277    
278                    if (!callbacks.containsKey(modId))
279                    {
280                        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);
281                        continue;
282                    }
283    
284                    NBTTagList tickets = ticketHolder.getTagList("Tickets");
285                    for (int j = 0; j < tickets.tagCount(); j++)
286                    {
287                        NBTTagCompound ticket = (NBTTagCompound) tickets.tagAt(j);
288                        Type type = Type.values()[ticket.getByte("Type")];
289                        byte ticketChunkDepth = ticket.getByte("ChunkListDepth");
290                        Ticket tick = new Ticket(modId, type, world);
291                        if (ticket.hasKey("ModData"))
292                        {
293                            tick.modData = ticket.getCompoundTag("ModData");
294                        }
295                        if (type == Type.ENTITY)
296                        {
297                            tick.entityChunkX = ticket.getInteger("chunkX");
298                            tick.entityChunkZ = ticket.getInteger("chunkZ");
299                            UUID uuid = new UUID(ticket.getLong("PersistentIDMSB"), ticket.getLong("PersistentIDLSB"));
300                            // add the ticket to the "pending entity" list
301                            pendingEntities.put(uuid, tick);
302                        }
303                        loadedTickets.put(modId, tick);
304                    }
305                }
306    
307                for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
308                {
309                    if (tick.ticketType == Type.ENTITY && tick.entity == null)
310                    {
311                        // force the world to load the entity's chunk
312                        // the load will come back through the loadEntity method and attach the entity
313                        // to the ticket
314                        world.getChunkFromChunkCoords(tick.entityChunkX, tick.entityChunkZ);
315                    }
316                }
317                for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
318                {
319                    if (tick.ticketType == Type.ENTITY && tick.entity == null)
320                    {
321                        FMLLog.warning("Failed to load persistent chunkloading entity %s from store.", pendingEntities.inverse().get(tick));
322                        loadedTickets.remove(tick.modId, tick);
323                    }
324                }
325                pendingEntities.clear();
326                // send callbacks
327                for (String modId : loadedTickets.keySet())
328                {
329                    LoadingCallback loadingCallback = callbacks.get(modId);
330                    int maxTicketLength = getMaxTicketLengthFor(modId);
331                    List<Ticket> tickets = loadedTickets.get(modId);
332                    if (loadingCallback instanceof OrderedLoadingCallback)
333                    {
334                        OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback;
335                        tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength);
336                    }
337                    if (tickets.size() > maxTicketLength)
338                    {
339                        FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size());
340                        tickets.subList(maxTicketLength, tickets.size()).clear();
341                    }
342                    ForgeChunkManager.tickets.get(world).putAll(modId, tickets);
343                    loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
344                }
345            }
346        }
347    
348        /**
349         * Set a chunkloading callback for the supplied mod object
350         *
351         * @param mod  The mod instance registering the callback
352         * @param callback The code to call back when forced chunks are loaded
353         */
354        public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback)
355        {
356            ModContainer container = getContainer(mod);
357            if (container == null)
358            {
359                FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
360                return;
361            }
362    
363            callbacks.put(container.getModId(), callback);
364        }
365    
366        /**
367         * Discover the available tickets for the mod in the world
368         *
369         * @param mod The mod that will own the tickets
370         * @param world The world
371         * @return The count of tickets left for the mod in the supplied world
372         */
373        public static int ticketCountAvailableFor(Object mod, World world)
374        {
375            ModContainer container = getContainer(mod);
376            if (container!=null)
377            {
378                String modId = container.getModId();
379                int allowedCount = getMaxTicketLengthFor(modId);
380                return allowedCount - tickets.get(world).get(modId).size();
381            }
382            else
383            {
384                return 0;
385            }
386        }
387    
388        private static ModContainer getContainer(Object mod)
389        {
390            ModContainer container = Loader.instance().getModObjectList().inverse().get(mod);
391            return container;
392        }
393    
394        private static int getMaxTicketLengthFor(String modId)
395        {
396            int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount;
397            return allowedCount;
398        }
399    
400        private static int getMaxChunkDepthFor(String modId)
401        {
402            int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks;
403            return allowedCount;
404        }
405        /**
406         * Request a chunkloading ticket of the appropriate type for the supplied mod
407         *
408         * @param mod The mod requesting a ticket
409         * @param world The world in which it is requesting the ticket
410         * @param type The type of ticket
411         * @return A ticket with which to register chunks for loading, or null if no further tickets are available
412         */
413        public static Ticket requestTicket(Object mod, World world, Type type)
414        {
415            ModContainer container = getContainer(mod);
416            if (container == null)
417            {
418                FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
419                return null;
420            }
421            String modId = container.getModId();
422            if (!callbacks.containsKey(modId))
423            {
424                FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId);
425                throw new RuntimeException("Invalid ticket request");
426            }
427    
428            int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount;
429    
430            if (tickets.get(world).get(modId).size() >= allowedCount)
431            {
432                FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
433                return null;
434            }
435            Ticket ticket = new Ticket(modId, type, world);
436            tickets.get(world).put(modId, ticket);
437    
438            return ticket;
439        }
440    
441        /**
442         * 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.
443         *
444         * @param ticket The ticket to release
445         */
446        public static void releaseTicket(Ticket ticket)
447        {
448            if (ticket == null)
449            {
450                return;
451            }
452            if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
453            {
454                return;
455            }
456            if (ticket.requestedChunks!=null)
457            {
458                for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
459                {
460                    unforceChunk(ticket, chunk);
461                }
462            }
463            tickets.get(ticket.world).remove(ticket.modId, ticket);
464        }
465    
466        /**
467         * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
468         * recently registered chunk is unforced and may be unloaded.
469         * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
470         *
471         * @param ticket The ticket registering the chunk
472         * @param chunk The chunk to force
473         */
474        public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
475        {
476            if (ticket == null || chunk == null)
477            {
478                return;
479            }
480            if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
481            {
482                throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
483            }
484            if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
485            {
486                FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
487                return;
488            }
489            ticket.requestedChunks.add(chunk);
490            forcedChunks.get(ticket.world).put(chunk, ticket);
491            if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
492            {
493                ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
494                unforceChunk(ticket,removed);
495            }
496        }
497    
498        /**
499         * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
500         * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
501         * in the ticket list
502         *
503         * @param ticket The ticket holding the chunk list
504         * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
505         */
506        public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
507        {
508            if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
509            {
510                return;
511            }
512            ticket.requestedChunks.remove(chunk);
513            ticket.requestedChunks.add(chunk);
514        }
515        /**
516         * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
517         *
518         * @param ticket The ticket holding the chunk
519         * @param chunk The chunk to unforce
520         */
521        public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
522        {
523            if (ticket == null || chunk == null)
524            {
525                return;
526            }
527            ticket.requestedChunks.remove(chunk);
528            forcedChunks.get(ticket.world).remove(chunk, ticket);
529        }
530    
531        static void loadConfiguration(File configDir)
532        {
533            Configuration config = new Configuration(new File(configDir,"forgeChunkLoading.cfg"), true);
534            try
535            {
536                config.categories.clear();
537                config.load();
538                config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
539                Property maxTicketCount = config.getOrCreateIntProperty("maximumTicketCount", "defaults", 200);
540                maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
541                        "in this file. This is the number of chunk loading requests a mod is allowed to make.";
542                defaultMaxCount = maxTicketCount.getInt(200);
543    
544                Property maxChunks = config.getOrCreateIntProperty("maximumChunksPerTicket", "defaults", 25);
545                maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
546                        "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
547                defaultMaxChunks = maxChunks.getInt(25);
548    
549                Property dormantChunkCacheSize = config.getOrCreateIntProperty("dormantChunkCacheSize", "defaults", 0);
550                dormantChunkCacheSize.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
551                        "loading times. Specify the size of that cache here";
552                dormantChunkCache = CacheBuilder.newBuilder().maximumSize(dormantChunkCacheSize.getInt(0)).build();
553                FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSize.getInt(0));
554    
555                Property modOverridesEnabled = config.getOrCreateBooleanProperty("enabled", "defaults", true);
556                modOverridesEnabled.comment = "Are mod overrides enabled?";
557                overridesEnabled = modOverridesEnabled.getBoolean(true);
558    
559                config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
560                        "Copy this section and rename the with the modid for the mod you wish to override.\n" +
561                        "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
562                        "for that mod");
563    
564                Property sampleTC = config.getOrCreateIntProperty("maximumTicketCount", "Forge", 200);
565                sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
566                sampleTC = config.getOrCreateIntProperty("maximumChunksPerTicket", "Forge", 25);
567                sampleTC.comment = "Maximum chunks per ticket for the mod.";
568                for (String mod : config.categories.keySet())
569                {
570                    if (mod.equals("Forge") || mod.equals("defaults"))
571                    {
572                        continue;
573                    }
574                    Property modTC = config.getOrCreateIntProperty("maximumTicketCount", mod, 200);
575                    Property modCPT = config.getOrCreateIntProperty("maximumChunksPerTicket", mod, 25);
576                    ticketConstraints.put(mod, modTC.getInt(200));
577                    chunkConstraints.put(mod, modCPT.getInt(25));
578                }
579            }
580            finally
581            {
582                config.save();
583            }
584        }
585    
586        /**
587         * The list of persistent chunks in the world. This set is immutable.
588         * @param world
589         * @return
590         */
591        public static SetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
592        {
593            return forcedChunks.containsKey(world) ? ImmutableSetMultimap.copyOf(forcedChunks.get(world)) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
594        }
595    
596        static void saveWorld(World world)
597        {
598            // only persist persistent worlds
599            if (!(world instanceof WorldServer)) { return; }
600            WorldServer worldServer = (WorldServer) world;
601            File chunkDir = worldServer.getChunkSaveLocation();
602            File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
603    
604            NBTTagCompound forcedChunkData = new NBTTagCompound();
605            NBTTagList ticketList = new NBTTagList();
606            forcedChunkData.setTag("TicketList", ticketList);
607    
608            Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
609            for (String modId : ticketSet.keySet())
610            {
611                NBTTagCompound ticketHolder = new NBTTagCompound();
612                ticketList.appendTag(ticketHolder);
613    
614                ticketHolder.setString("Owner", modId);
615                NBTTagList tickets = new NBTTagList();
616                ticketHolder.setTag("Tickets", tickets);
617    
618                for (Ticket tick : ticketSet.get(modId))
619                {
620                    NBTTagCompound ticket = new NBTTagCompound();
621                    tickets.appendTag(ticket);
622                    ticket.setByte("Type", (byte) tick.ticketType.ordinal());
623                    ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
624                    if (tick.modData != null)
625                    {
626                        ticket.setCompoundTag("ModData", tick.modData);
627                    }
628                    if (tick.ticketType == Type.ENTITY)
629                    {
630                        ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
631                        ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
632                        ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
633                        ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
634                    }
635                }
636            }
637            try
638            {
639                CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
640            }
641            catch (IOException e)
642            {
643                FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
644                return;
645            }
646        }
647    
648        static void loadEntity(Entity entity)
649        {
650            UUID id = entity.getPersistentID();
651            Ticket tick = pendingEntities.get(id);
652            if (tick != null)
653            {
654                tick.bindEntity(entity);
655                pendingEntities.remove(id);
656            }
657        }
658    
659        public static void putDormantChunk(long coords, Chunk chunk)
660        {
661            dormantChunkCache.put(coords, chunk);
662        }
663    
664        public static Chunk fetchDormantChunk(long coords)
665        {
666            return dormantChunkCache.getIfPresent(coords);
667        }
668    }