    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                if (loadingCallback == null)
    502                {
    503                    continue;
    504                }
    505                int maxTicketLength = getMaxTicketLengthFor(modId);
    506                List<Ticket> tickets = loadedTickets.get(modId);
    507                if (loadingCallback instanceof OrderedLoadingCallback)
    508                {
    509                    OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback;
    510                    tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength);
    511                }
    512                if (tickets.size() > maxTicketLength)
    513                {
    514                    FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size());
    515                    tickets.subList(maxTicketLength, tickets.size()).clear();
    516                }
    517                ForgeChunkManager.tickets.get(world).putAll(modId, tickets);
    518                loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
    519            }
    520            for (String modId : playerLoadedTickets.keySet())
    521            {
    522                LoadingCallback loadingCallback = callbacks.get(modId);
    523                if (loadingCallback == null)
    524                {
    525                    continue;
    526                }
    527                ListMultimap<String,Ticket> tickets = playerLoadedTickets.get(modId);
    528                if (loadingCallback instanceof PlayerOrderedLoadingCallback)
    529                {
    530                    PlayerOrderedLoadingCallback orderedLoadingCallback = (PlayerOrderedLoadingCallback) loadingCallback;
    531                    tickets = orderedLoadingCallback.playerTicketsLoaded(ImmutableListMultimap.copyOf(tickets), world);
    532                    playerTickets.putAll(tickets);
    533                }
    534                ForgeChunkManager.tickets.get(world).putAll("Forge", tickets.values());
    535                loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets.values()), world);
    536            }
    537        }
    538    }
    539
    540    static void unloadWorld(World world)
    541    {
    542        // World save fires before this event so the chunk loading info will be done
    543        if (!(world instanceof WorldServer))
    544        {
    545            return;
    546        }
    547
    548        forcedChunks.remove(world);
    549        dormantChunkCache.remove(world);
    550     // integrated server is shutting down
    551        if (!MinecraftServer.getServer().isServerRunning())
    552        {
    553            playerTickets.clear();
    554            tickets.clear();
    555        }
    556    }
    557
    558    /**
    559     * Set a chunkloading callback for the supplied mod object
    560     *
    561     * @param mod  The mod instance registering the callback
    562     * @param callback The code to call back when forced chunks are loaded
    563     */
    564    public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback)
    565    {
    566        ModContainer container = getContainer(mod);
    567        if (container == null)
    568        {
    569            FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
    570            return;
    571        }
    572
    573        callbacks.put(container.getModId(), callback);
    574    }
    575
    576    /**
    577     * Discover the available tickets for the mod in the world
    578     *
    579     * @param mod The mod that will own the tickets
    580     * @param world The world
    581     * @return The count of tickets left for the mod in the supplied world
    582     */
    583    public static int ticketCountAvailableFor(Object mod, World world)
    584    {
    585        ModContainer container = getContainer(mod);
    586        if (container!=null)
    587        {
    588            String modId = container.getModId();
    589            int allowedCount = getMaxTicketLengthFor(modId);
    590            return allowedCount - tickets.get(world).get(modId).size();
    591        }
    592        else
    593        {
    594            return 0;
    595        }
    596    }
    597
    598    private static ModContainer getContainer(Object mod)
    599    {
    600        ModContainer container = Loader.instance().getModObjectList().inverse().get(mod);
    601        return container;
    602    }
    603
    604    public static int getMaxTicketLengthFor(String modId)
    605    {
    606        int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount;
    607        return allowedCount;
    608    }
    609
    610    public static int getMaxChunkDepthFor(String modId)
    611    {
    612        int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks;
    613        return allowedCount;
    614    }
    615
    616    public static int ticketCountAvailableFor(String username)
    617    {
    618        return playerTicketLength - playerTickets.get(username).size();
    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)
    665        {
    666            if (!warnedMods.contains(modId))
    667            {
    668                FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
    669                warnedMods.add(modId);
    670            }
    671            return null;
    672        }
    673        Ticket ticket = new Ticket(modId, type, world);
    674        tickets.get(world).put(modId, ticket);
    675
    676        return ticket;
    677    }
    678
    679    /**
    680     * 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.
    681     *
    682     * @param ticket The ticket to release
    683     */
    684    public static void releaseTicket(Ticket ticket)
    685    {
    686        if (ticket == null)
    687        {
    688            return;
    689        }
    690        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
    691        {
    692            return;
    693        }
    694        if (ticket.requestedChunks!=null)
    695        {
    696            for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
    697            {
    698                unforceChunk(ticket, chunk);
    699            }
    700        }
    701        if (ticket.isPlayerTicket())
    702        {
    703            playerTickets.remove(ticket.player, ticket);
    704            tickets.get(ticket.world).remove("Forge",ticket);
    705        }
    706        else
    707        {
    708            tickets.get(ticket.world).remove(ticket.modId, ticket);
    709        }
    710    }
    711
    712    /**
    713     * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
    714     * recently registered chunk is unforced and may be unloaded.
    715     * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
    716     *
    717     * @param ticket The ticket registering the chunk
    718     * @param chunk The chunk to force
    719     */
    720    public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
    721    {
    722        if (ticket == null || chunk == null)
    723        {
    724            return;
    725        }
    726        if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
    727        {
    728            throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
    729        }
    730        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
    731        {
    732            FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
    733            return;
    734        }
    735        ticket.requestedChunks.add(chunk);
    736        MinecraftForge.EVENT_BUS.post(new ForceChunkEvent(ticket, chunk));
    737
    738        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build();
    739        forcedChunks.put(ticket.world, newMap);
    740        if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
    741        {
    742            ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
    743            unforceChunk(ticket,removed);
    744        }
    745    }
    746
    747    /**
    748     * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
    749     * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
    750     * in the ticket list
    751     *
    752     * @param ticket The ticket holding the chunk list
    753     * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
    754     */
    755    public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
    756    {
    757        if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
    758        {
    759            return;
    760        }
    761        ticket.requestedChunks.remove(chunk);
    762        ticket.requestedChunks.add(chunk);
    763    }
    764    /**
    765     * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
    766     *
    767     * @param ticket The ticket holding the chunk
    768     * @param chunk The chunk to unforce
    769     */
    770    public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
    771    {
    772        if (ticket == null || chunk == null)
    773        {
    774            return;
    775        }
    776        ticket.requestedChunks.remove(chunk);
    777        MinecraftForge.EVENT_BUS.post(new UnforceChunkEvent(ticket, chunk));
    778        LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world));
    779        copy.remove(chunk, ticket);
    780        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy);
    781        forcedChunks.put(ticket.world,newMap);
    782    }
    783
    784    static void loadConfiguration()
    785    {
    786        for (String mod : config.getCategoryNames())
    787        {
    788            if (mod.equals("Forge") || mod.equals("defaults"))
    789            {
    790                continue;
    791            }
    792            Property modTC = config.get(mod, "maximumTicketCount", 200);
    793            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
    794            ticketConstraints.put(mod, modTC.getInt(200));
    795            chunkConstraints.put(mod, modCPT.getInt(25));
    796        }
    797        if (config.hasChanged())
    798        {
    799            config.save();
    800        }
    801    }
    802
    803    /**
    804     * The list of persistent chunks in the world. This set is immutable.
    805     * @param world
    806     * @return the list of persistent chunks in the world
    807     */
    808    public static ImmutableSetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
    809    {
    810        return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
    811    }
    812
    813    static void saveWorld(World world)
    814    {
    815        // only persist persistent worlds
    816        if (!(world instanceof WorldServer)) { return; }
    817        WorldServer worldServer = (WorldServer) world;
    818        File chunkDir = worldServer.getChunkSaveLocation();
    819        File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
    820
    821        NBTTagCompound forcedChunkData = new NBTTagCompound();
    822        NBTTagList ticketList = new NBTTagList();
    823        forcedChunkData.setTag("TicketList", ticketList);
    824
    825        Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
    826        for (String modId : ticketSet.keySet())
    827        {
    828            NBTTagCompound ticketHolder = new NBTTagCompound();
    829            ticketList.appendTag(ticketHolder);
    830
    831            ticketHolder.setString("Owner", modId);
    832            NBTTagList tickets = new NBTTagList();
    833            ticketHolder.setTag("Tickets", tickets);
    834
    835            for (Ticket tick : ticketSet.get(modId))
    836            {
    837                NBTTagCompound ticket = new NBTTagCompound();
    838                ticket.setByte("Type", (byte) tick.ticketType.ordinal());
    839                ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
    840                if (tick.isPlayerTicket())
    841                {
    842                    ticket.setString("ModId", tick.modId);
    843                    ticket.setString("Player", tick.player);
    844                }
    845                if (tick.modData != null)
    846                {
    847                    ticket.setCompoundTag("ModData", tick.modData);
    848                }
    849                if (tick.ticketType == Type.ENTITY && tick.entity != null && tick.entity.addEntityID(new NBTTagCompound()))
    850                {
    851                    ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
    852                    ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
    853                    ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
    854                    ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
    855                    tickets.appendTag(ticket);
    856                }
    857                else if (tick.ticketType != Type.ENTITY)
    858                {
    859                    tickets.appendTag(ticket);
    860                }
    861            }
    862        }
    863        try
    864        {
    865            CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
    866        }
    867        catch (IOException e)
    868        {
    869            FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
    870            return;
    871        }
    872    }
    873
    874    static void loadEntity(Entity entity)
    875    {
    876        UUID id = entity.getPersistentID();
    877        Ticket tick = pendingEntities.get(id);
    878        if (tick != null)
    879        {
    880            tick.bindEntity(entity);
    881            pendingEntities.remove(id);
    882        }
    883    }
    884
    885    public static void putDormantChunk(long coords, Chunk chunk)
    886    {
    887        Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
    888        if (cache != null)
    889        {
    890            cache.put(coords, chunk);
    891        }
    892    }
    893
    894    public static Chunk fetchDormantChunk(long coords, World world)
    895    {
    896        Cache<Long, Chunk> cache = dormantChunkCache.get(world);
    897        if (cache == null)
    898        {
    899            return null;
    900        }
    901        Chunk chunk = cache.getIfPresent(coords);
    902        if (chunk != null)
    903        {
    904            for (List<Entity> eList : chunk.entityLists)
    905            {
    906                for (Entity e: eList)
    907                {
    908                    e.resetEntityId();
    909                }
    910            }
    911        }
    912        return chunk;
    913    }
    914
    915    static void captureConfig(File configDir)
    916    {
    917        cfgFile = new File(configDir,"forgeChunkLoading.cfg");
    918        config = new Configuration(cfgFile, true);
    919        try
    920        {
    921            config.load();
    922        }
    923        catch (Exception e)
    924        {
    925            File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
    926            if (dest.exists())
    927            {
    928                dest.delete();
    929            }
    930            cfgFile.renameTo(dest);
    931            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");
    932        }
    933        config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
    934        Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
    935        maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
    936                    "in this file. This is the number of chunk loading requests a mod is allowed to make.";
    937        defaultMaxCount = maxTicketCount.getInt(200);
    938
    939        Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
    940        maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
    941                    "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
    942        defaultMaxChunks = maxChunks.getInt(25);
    943
    944        Property playerTicketCount = config.get("defaults", "playerTicketCount", 500);
    945        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.";
    946        playerTicketLength = playerTicketCount.getInt(500);
    947
    948        Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
    949        dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
    950                    "loading times. Specify the size (in chunks) of that cache here";
    951        dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
    952        FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
    953
    954        Property modOverridesEnabled = config.get("defaults", "enabled", true);
    955        modOverridesEnabled.comment = "Are mod overrides enabled?";
    956        overridesEnabled = modOverridesEnabled.getBoolean(true);
    957
    958        config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
    959                "Copy this section and rename the with the modid for the mod you wish to override.\n" +
    960                "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
    961                "for that mod");
    962
    963        Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
    964        sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
    965        sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
    966        sampleTC.comment = "Maximum chunks per ticket for the mod.";
    967        for (String mod : config.getCategoryNames())
    968        {
    969            if (mod.equals("Forge") || mod.equals("defaults"))
    970            {
    971                continue;
    972            }
    973            Property modTC = config.get(mod, "maximumTicketCount", 200);
    974            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
    975        }
    976    }
    977
    978
    979    public static ConfigCategory getConfigFor(Object mod)
    980    {
    981        ModContainer container = getContainer(mod);
    982        if (container != null)
    983        {
    984            return config.getCategory(container.getModId());
    985        }
    986
    987        return null;
    988    }
    989
    990    public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
    991    {
    992        ModContainer container = getContainer(mod);
    993        if (container != null)
    994        {
    995            ConfigCategory cat = config.getCategory(container.getModId());
    996            cat.put(propertyName, new Property(propertyName, value, type));
    997        }
    998    }
    999}