    001package net.minecraftforge.common;
    002
    003import java.io.DataInputStream;
    004import java.io.File;
    005import java.io.FileInputStream;
    006import java.io.IOException;
    007import java.util.HashSet;
    008import java.util.LinkedHashSet;
    009import java.util.LinkedList;
    010import java.util.List;
    011import java.util.Map;
    012import java.util.Set;
    013import java.util.UUID;
    014import java.util.logging.Level;
    015
    016import com.google.common.base.Supplier;
    017import com.google.common.base.Suppliers;
    018import com.google.common.cache.Cache;
    019import com.google.common.cache.CacheBuilder;
    020import com.google.common.collect.ArrayListMultimap;
    021import com.google.common.collect.BiMap;
    022import com.google.common.collect.ForwardingSet;
    023import com.google.common.collect.HashBiMap;
    024import com.google.common.collect.HashMultimap;
    025import com.google.common.collect.ImmutableList;
    026import com.google.common.collect.ImmutableListMultimap;
    027import com.google.common.collect.ImmutableSet;
    028import com.google.common.collect.ImmutableSetMultimap;
    029import com.google.common.collect.LinkedHashMultimap;
    030import com.google.common.collect.ListMultimap;
    031import com.google.common.collect.Lists;
    032import com.google.common.collect.MapMaker;
    033import com.google.common.collect.Maps;
    034import com.google.common.collect.Multimap;
    035import com.google.common.collect.Multimaps;
    036import com.google.common.collect.Multiset;
    037import com.google.common.collect.SetMultimap;
    038import com.google.common.collect.Sets;
    039import com.google.common.collect.TreeMultiset;
    040
    041import cpw.mods.fml.common.FMLLog;
    042import cpw.mods.fml.common.Loader;
    043import cpw.mods.fml.common.ModContainer;
    044
    045import net.minecraft.server.MinecraftServer;
    046import net.minecraft.world.chunk.Chunk;
    047import net.minecraft.world.ChunkCoordIntPair;
    048import net.minecraft.nbt.CompressedStreamTools;
    049import net.minecraft.entity.Entity;
    050import net.minecraft.entity.player.EntityPlayer;
    051import net.minecraft.util.MathHelper;
    052import net.minecraft.nbt.NBTBase;
    053import net.minecraft.nbt.NBTTagCompound;
    054import net.minecraft.nbt.NBTTagList;
    055import net.minecraft.world.World;
    056import net.minecraft.world.WorldServer;
    057import net.minecraftforge.common.ForgeChunkManager.Ticket;
    058import 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 */
    077public 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 the entity
    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    public static int ticketCountAvailableFor(String username)
    609    {
    610        return playerTicketLength - playerTickets.get(username).size();
    611    }
    612
    613    public static Ticket requestPlayerTicket(Object mod, String player, World world, Type type)
    614    {
    615        ModContainer mc = getContainer(mod);
    616        if (mc == null)
    617        {
    618            FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
    619            return null;
    620        }
    621        if (playerTickets.get(player).size()>playerTicketLength)
    622        {
    623            FMLLog.warning("Unable to assign further chunkloading tickets to player %s (on behalf of mod %s)", player, mc.getModId());
    624            return null;
    625        }
    626        Ticket ticket = new Ticket(mc.getModId(),type,world,player);
    627        playerTickets.put(player, ticket);
    628        tickets.get(world).put("Forge", ticket);
    629        return ticket;
    630    }
    631    /**
    632     * Request a chunkloading ticket of the appropriate type for the supplied mod
    633     *
    634     * @param mod The mod requesting a ticket
    635     * @param world The world in which it is requesting the ticket
    636     * @param type The type of ticket
    637     * @return A ticket with which to register chunks for loading, or null if no further tickets are available
    638     */
    639    public static Ticket requestTicket(Object mod, World world, Type type)
    640    {
    641        ModContainer container = getContainer(mod);
    642        if (container == null)
    643        {
    644            FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
    645            return null;
    646        }
    647        String modId = container.getModId();
    648        if (!callbacks.containsKey(modId))
    649        {
    650            FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId);
    651            throw new RuntimeException("Invalid ticket request");
    652        }
    653
    654        int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount;
    655
    656        if (tickets.get(world).get(modId).size() >= allowedCount)
    657        {
    658            if (!warnedMods.contains(modId))
    659            {
    660                FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
    661                warnedMods.add(modId);
    662            }
    663            return null;
    664        }
    665        Ticket ticket = new Ticket(modId, type, world);
    666        tickets.get(world).put(modId, ticket);
    667
    668        return ticket;
    669    }
    670
    671    /**
    672     * 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.
    673     *
    674     * @param ticket The ticket to release
    675     */
    676    public static void releaseTicket(Ticket ticket)
    677    {
    678        if (ticket == null)
    679        {
    680            return;
    681        }
    682        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
    683        {
    684            return;
    685        }
    686        if (ticket.requestedChunks!=null)
    687        {
    688            for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
    689            {
    690                unforceChunk(ticket, chunk);
    691            }
    692        }
    693        if (ticket.isPlayerTicket())
    694        {
    695            playerTickets.remove(ticket.player, ticket);
    696            tickets.get(ticket.world).remove("Forge",ticket);
    697        }
    698        else
    699        {
    700            tickets.get(ticket.world).remove(ticket.modId, ticket);
    701        }
    702    }
    703
    704    /**
    705     * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
    706     * recently registered chunk is unforced and may be unloaded.
    707     * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
    708     *
    709     * @param ticket The ticket registering the chunk
    710     * @param chunk The chunk to force
    711     */
    712    public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
    713    {
    714        if (ticket == null || chunk == null)
    715        {
    716            return;
    717        }
    718        if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
    719        {
    720            throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
    721        }
    722        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
    723        {
    724            FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
    725            return;
    726        }
    727        ticket.requestedChunks.add(chunk);
    728        MinecraftForge.EVENT_BUS.post(new ForceChunkEvent(ticket, chunk));
    729
    730        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build();
    731        forcedChunks.put(ticket.world, newMap);
    732        if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
    733        {
    734            ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
    735            unforceChunk(ticket,removed);
    736        }
    737    }
    738
    739    /**
    740     * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
    741     * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
    742     * in the ticket list
    743     *
    744     * @param ticket The ticket holding the chunk list
    745     * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
    746     */
    747    public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
    748    {
    749        if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
    750        {
    751            return;
    752        }
    753        ticket.requestedChunks.remove(chunk);
    754        ticket.requestedChunks.add(chunk);
    755    }
    756    /**
    757     * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
    758     *
    759     * @param ticket The ticket holding the chunk
    760     * @param chunk The chunk to unforce
    761     */
    762    public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
    763    {
    764        if (ticket == null || chunk == null)
    765        {
    766            return;
    767        }
    768        ticket.requestedChunks.remove(chunk);
    769        MinecraftForge.EVENT_BUS.post(new UnforceChunkEvent(ticket, chunk));
    770        LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world));
    771        copy.remove(chunk, ticket);
    772        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy);
    773        forcedChunks.put(ticket.world,newMap);
    774    }
    775
    776    static void loadConfiguration()
    777    {
    778        for (String mod : config.getCategoryNames())
    779        {
    780            if (mod.equals("Forge") || mod.equals("defaults"))
    781            {
    782                continue;
    783            }
    784            Property modTC = config.get(mod, "maximumTicketCount", 200);
    785            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
    786            ticketConstraints.put(mod, modTC.getInt(200));
    787            chunkConstraints.put(mod, modCPT.getInt(25));
    788        }
    789        if (config.hasChanged())
    790        {
    791            config.save();
    792        }
    793    }
    794
    795    /**
    796     * The list of persistent chunks in the world. This set is immutable.
    797     * @param world
    798     * @return the list of persistent chunks in the world
    799     */
    800    public static ImmutableSetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
    801    {
    802        return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
    803    }
    804
    805    static void saveWorld(World world)
    806    {
    807        // only persist persistent worlds
    808        if (!(world instanceof WorldServer)) { return; }
    809        WorldServer worldServer = (WorldServer) world;
    810        File chunkDir = worldServer.getChunkSaveLocation();
    811        File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
    812
    813        NBTTagCompound forcedChunkData = new NBTTagCompound();
    814        NBTTagList ticketList = new NBTTagList();
    815        forcedChunkData.setTag("TicketList", ticketList);
    816
    817        Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
    818        for (String modId : ticketSet.keySet())
    819        {
    820            NBTTagCompound ticketHolder = new NBTTagCompound();
    821            ticketList.appendTag(ticketHolder);
    822
    823            ticketHolder.setString("Owner", modId);
    824            NBTTagList tickets = new NBTTagList();
    825            ticketHolder.setTag("Tickets", tickets);
    826
    827            for (Ticket tick : ticketSet.get(modId))
    828            {
    829                NBTTagCompound ticket = new NBTTagCompound();
    830                ticket.setByte("Type", (byte) tick.ticketType.ordinal());
    831                ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
    832                if (tick.isPlayerTicket())
    833                {
    834                    ticket.setString("ModId", tick.modId);
    835                    ticket.setString("Player", tick.player);
    836                }
    837                if (tick.modData != null)
    838                {
    839                    ticket.setCompoundTag("ModData", tick.modData);
    840                }
    841                if (tick.ticketType == Type.ENTITY && tick.entity != null && tick.entity.addEntityID(new NBTTagCompound()))
    842                {
    843                    ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
    844                    ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
    845                    ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
    846                    ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
    847                    tickets.appendTag(ticket);
    848                }
    849                else if (tick.ticketType != Type.ENTITY)
    850                {
    851                    tickets.appendTag(ticket);
    852                }
    853            }
    854        }
    855        try
    856        {
    857            CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
    858        }
    859        catch (IOException e)
    860        {
    861            FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
    862            return;
    863        }
    864    }
    865
    866    static void loadEntity(Entity entity)
    867    {
    868        UUID id = entity.getPersistentID();
    869        Ticket tick = pendingEntities.get(id);
    870        if (tick != null)
    871        {
    872            tick.bindEntity(entity);
    873            pendingEntities.remove(id);
    874        }
    875    }
    876
    877    public static void putDormantChunk(long coords, Chunk chunk)
    878    {
    879        Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
    880        if (cache != null)
    881        {
    882            cache.put(coords, chunk);
    883        }
    884    }
    885
    886    public static Chunk fetchDormantChunk(long coords, World world)
    887    {
    888        Cache<Long, Chunk> cache = dormantChunkCache.get(world);
    889        if (cache == null)
    890        {
    891            return null;
    892        }
    893        Chunk chunk = cache.getIfPresent(coords);
    894        if (chunk != null)
    895        {
    896            for (List<Entity> eList : chunk.entityLists)
    897            {
    898                for (Entity e: eList)
    899                {
    900                    e.resetEntityId();
    901                }
    902            }
    903        }
    904        return chunk;
    905    }
    906
    907    static void captureConfig(File configDir)
    908    {
    909        cfgFile = new File(configDir,"forgeChunkLoading.cfg");
    910        config = new Configuration(cfgFile, true);
    911        try
    912        {
    913            config.load();
    914        }
    915        catch (Exception e)
    916        {
    917            File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
    918            if (dest.exists())
    919            {
    920                dest.delete();
    921            }
    922            cfgFile.renameTo(dest);
    923            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");
    924        }
    925        config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
    926        Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
    927        maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
    928                    "in this file. This is the number of chunk loading requests a mod is allowed to make.";
    929        defaultMaxCount = maxTicketCount.getInt(200);
    930
    931        Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
    932        maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
    933                    "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
    934        defaultMaxChunks = maxChunks.getInt(25);
    935
    936        Property playerTicketCount = config.get("defaults", "playerTicketCount", 500);
    937        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.";
    938        playerTicketLength = playerTicketCount.getInt(500);
    939
    940        Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
    941        dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
    942                    "loading times. Specify the size (in chunks) of that cache here";
    943        dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
    944        FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
    945
    946        Property modOverridesEnabled = config.get("defaults", "enabled", true);
    947        modOverridesEnabled.comment = "Are mod overrides enabled?";
    948        overridesEnabled = modOverridesEnabled.getBoolean(true);
    949
    950        config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
    951                "Copy this section and rename the with the modid for the mod you wish to override.\n" +
    952                "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
    953                "for that mod");
    954
    955        Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
    956        sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
    957        sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
    958        sampleTC.comment = "Maximum chunks per ticket for the mod.";
    959        for (String mod : config.getCategoryNames())
    960        {
    961            if (mod.equals("Forge") || mod.equals("defaults"))
    962            {
    963                continue;
    964            }
    965            Property modTC = config.get(mod, "maximumTicketCount", 200);
    966            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
    967        }
    968    }
    969
    970
    971    public static ConfigCategory getConfigFor(Object mod)
    972    {
    973        ModContainer container = getContainer(mod);
    974        if (container != null)
    975        {
    976            return config.getCategory(container.getModId());
    977        }
    978
    979        return null;
    980    }
    981
    982    public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
    983    {
    984        ModContainer container = getContainer(mod);
    985        if (container != null)
    986        {
    987            ConfigCategory cat = config.getCategory(container.getModId());
    988            cat.put(propertyName, new Property(propertyName, value, type));
    989        }
    990    }
    991}