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