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