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