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 && !warnedMods.contains(modId))
657        {
658            FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
659            warnedMods.add(modId);
660            return null;
661        }
662        Ticket ticket = new Ticket(modId, type, world);
663        tickets.get(world).put(modId, ticket);
664
665        return ticket;
666    }
667
668    /**
669     * 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.
670     *
671     * @param ticket The ticket to release
672     */
673    public static void releaseTicket(Ticket ticket)
674    {
675        if (ticket == null)
676        {
677            return;
678        }
679        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
680        {
681            return;
682        }
683        if (ticket.requestedChunks!=null)
684        {
685            for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
686            {
687                unforceChunk(ticket, chunk);
688            }
689        }
690        if (ticket.isPlayerTicket())
691        {
692            playerTickets.remove(ticket.player, ticket);
693            tickets.get(ticket.world).remove("Forge",ticket);
694        }
695        else
696        {
697            tickets.get(ticket.world).remove(ticket.modId, ticket);
698        }
699    }
700
701    /**
702     * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
703     * recently registered chunk is unforced and may be unloaded.
704     * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
705     *
706     * @param ticket The ticket registering the chunk
707     * @param chunk The chunk to force
708     */
709    public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
710    {
711        if (ticket == null || chunk == null)
712        {
713            return;
714        }
715        if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
716        {
717            throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
718        }
719        if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
720        {
721            FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
722            return;
723        }
724        ticket.requestedChunks.add(chunk);
725        MinecraftForge.EVENT_BUS.post(new ForceChunkEvent(ticket, chunk));
726
727        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build();
728        forcedChunks.put(ticket.world, newMap);
729        if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
730        {
731            ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
732            unforceChunk(ticket,removed);
733        }
734    }
735
736    /**
737     * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
738     * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
739     * in the ticket list
740     *
741     * @param ticket The ticket holding the chunk list
742     * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
743     */
744    public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
745    {
746        if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
747        {
748            return;
749        }
750        ticket.requestedChunks.remove(chunk);
751        ticket.requestedChunks.add(chunk);
752    }
753    /**
754     * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
755     *
756     * @param ticket The ticket holding the chunk
757     * @param chunk The chunk to unforce
758     */
759    public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
760    {
761        if (ticket == null || chunk == null)
762        {
763            return;
764        }
765        ticket.requestedChunks.remove(chunk);
766        MinecraftForge.EVENT_BUS.post(new UnforceChunkEvent(ticket, chunk));
767        LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world));
768        copy.remove(chunk, ticket);
769        ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy);
770        forcedChunks.put(ticket.world,newMap);
771    }
772
773    static void loadConfiguration()
774    {
775        for (String mod : config.categories.keySet())
776        {
777            if (mod.equals("Forge") || mod.equals("defaults"))
778            {
779                continue;
780            }
781            Property modTC = config.get(mod, "maximumTicketCount", 200);
782            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
783            ticketConstraints.put(mod, modTC.getInt(200));
784            chunkConstraints.put(mod, modCPT.getInt(25));
785        }
786        config.save();
787    }
788
789    /**
790     * The list of persistent chunks in the world. This set is immutable.
791     * @param world
792     * @return the list of persistent chunks in the world
793     */
794    public static ImmutableSetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
795    {
796        return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
797    }
798
799    static void saveWorld(World world)
800    {
801        // only persist persistent worlds
802        if (!(world instanceof WorldServer)) { return; }
803        WorldServer worldServer = (WorldServer) world;
804        File chunkDir = worldServer.getChunkSaveLocation();
805        File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
806
807        NBTTagCompound forcedChunkData = new NBTTagCompound();
808        NBTTagList ticketList = new NBTTagList();
809        forcedChunkData.setTag("TicketList", ticketList);
810
811        Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
812        for (String modId : ticketSet.keySet())
813        {
814            NBTTagCompound ticketHolder = new NBTTagCompound();
815            ticketList.appendTag(ticketHolder);
816
817            ticketHolder.setString("Owner", modId);
818            NBTTagList tickets = new NBTTagList();
819            ticketHolder.setTag("Tickets", tickets);
820
821            for (Ticket tick : ticketSet.get(modId))
822            {
823                NBTTagCompound ticket = new NBTTagCompound();
824                ticket.setByte("Type", (byte) tick.ticketType.ordinal());
825                ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
826                if (tick.isPlayerTicket())
827                {
828                    ticket.setString("ModId", tick.modId);
829                    ticket.setString("Player", tick.player);
830                }
831                if (tick.modData != null)
832                {
833                    ticket.setCompoundTag("ModData", tick.modData);
834                }
835                if (tick.ticketType == Type.ENTITY && tick.entity != null && tick.entity.addEntityID(new NBTTagCompound()))
836                {
837                    ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
838                    ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
839                    ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
840                    ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
841                    tickets.appendTag(ticket);
842                }
843                else if (tick.ticketType != Type.ENTITY)
844                {
845                    tickets.appendTag(ticket);
846                }
847            }
848        }
849        try
850        {
851            CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
852        }
853        catch (IOException e)
854        {
855            FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
856            return;
857        }
858    }
859
860    static void loadEntity(Entity entity)
861    {
862        UUID id = entity.getPersistentID();
863        Ticket tick = pendingEntities.get(id);
864        if (tick != null)
865        {
866            tick.bindEntity(entity);
867            pendingEntities.remove(id);
868        }
869    }
870
871    public static void putDormantChunk(long coords, Chunk chunk)
872    {
873        Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
874        if (cache != null)
875        {
876            cache.put(coords, chunk);
877        }
878    }
879
880    public static Chunk fetchDormantChunk(long coords, World world)
881    {
882        Cache<Long, Chunk> cache = dormantChunkCache.get(world);
883        return cache == null ? null : cache.getIfPresent(coords);
884    }
885
886    static void captureConfig(File configDir)
887    {
888        cfgFile = new File(configDir,"forgeChunkLoading.cfg");
889        config = new Configuration(cfgFile, true);
890        try
891        {
892            config.load();
893        }
894        catch (Exception e)
895        {
896            File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
897            if (dest.exists())
898            {
899                dest.delete();
900            }
901            cfgFile.renameTo(dest);
902            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");
903        }
904        config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
905        Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
906        maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
907                    "in this file. This is the number of chunk loading requests a mod is allowed to make.";
908        defaultMaxCount = maxTicketCount.getInt(200);
909
910        Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
911        maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
912                    "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
913        defaultMaxChunks = maxChunks.getInt(25);
914
915        Property playerTicketCount = config.get("defaults", "playerTicketCount", 500);
916        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.";
917        playerTicketLength = playerTicketCount.getInt(500);
918
919        Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
920        dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
921                    "loading times. Specify the size of that cache here";
922        dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
923        FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
924
925        Property modOverridesEnabled = config.get("defaults", "enabled", true);
926        modOverridesEnabled.comment = "Are mod overrides enabled?";
927        overridesEnabled = modOverridesEnabled.getBoolean(true);
928
929        config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
930                "Copy this section and rename the with the modid for the mod you wish to override.\n" +
931                "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
932                "for that mod");
933
934        Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
935        sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
936        sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
937        sampleTC.comment = "Maximum chunks per ticket for the mod.";
938        for (String mod : config.categories.keySet())
939        {
940            if (mod.equals("Forge") || mod.equals("defaults"))
941            {
942                continue;
943            }
944            Property modTC = config.get(mod, "maximumTicketCount", 200);
945            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
946        }
947    }
948
949
950    public static Map<String,Property> getConfigMapFor(Object mod)
951    {
952        ModContainer container = getContainer(mod);
953        if (container != null)
954        {
955            return config.getCategory(container.getModId()).getValues();
956        }
957
958        return null;
959    }
960
961    public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
962    {
963        ModContainer container = getContainer(mod);
964        if (container != null)
965        {
966            Map<String, Property> props = config.getCategory(container.getModId()).getValues();
967            props.put(propertyName, new Property(propertyName, value, type));
968        }
969    }
970}