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