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.categories.keySet())
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        config.save();
790    }
791
792    /**
793     * The list of persistent chunks in the world. This set is immutable.
794     * @param world
795     * @return the list of persistent chunks in the world
796     */
797    public static ImmutableSetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
798    {
799        return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
800    }
801
802    static void saveWorld(World world)
803    {
804        // only persist persistent worlds
805        if (!(world instanceof WorldServer)) { return; }
806        WorldServer worldServer = (WorldServer) world;
807        File chunkDir = worldServer.getChunkSaveLocation();
808        File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
809
810        NBTTagCompound forcedChunkData = new NBTTagCompound();
811        NBTTagList ticketList = new NBTTagList();
812        forcedChunkData.setTag("TicketList", ticketList);
813
814        Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
815        for (String modId : ticketSet.keySet())
816        {
817            NBTTagCompound ticketHolder = new NBTTagCompound();
818            ticketList.appendTag(ticketHolder);
819
820            ticketHolder.setString("Owner", modId);
821            NBTTagList tickets = new NBTTagList();
822            ticketHolder.setTag("Tickets", tickets);
823
824            for (Ticket tick : ticketSet.get(modId))
825            {
826                NBTTagCompound ticket = new NBTTagCompound();
827                ticket.setByte("Type", (byte) tick.ticketType.ordinal());
828                ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
829                if (tick.isPlayerTicket())
830                {
831                    ticket.setString("ModId", tick.modId);
832                    ticket.setString("Player", tick.player);
833                }
834                if (tick.modData != null)
835                {
836                    ticket.setCompoundTag("ModData", tick.modData);
837                }
838                if (tick.ticketType == Type.ENTITY && tick.entity != null && tick.entity.addEntityID(new NBTTagCompound()))
839                {
840                    ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
841                    ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
842                    ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
843                    ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
844                    tickets.appendTag(ticket);
845                }
846                else if (tick.ticketType != Type.ENTITY)
847                {
848                    tickets.appendTag(ticket);
849                }
850            }
851        }
852        try
853        {
854            CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
855        }
856        catch (IOException e)
857        {
858            FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
859            return;
860        }
861    }
862
863    static void loadEntity(Entity entity)
864    {
865        UUID id = entity.getPersistentID();
866        Ticket tick = pendingEntities.get(id);
867        if (tick != null)
868        {
869            tick.bindEntity(entity);
870            pendingEntities.remove(id);
871        }
872    }
873
874    public static void putDormantChunk(long coords, Chunk chunk)
875    {
876        Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
877        if (cache != null)
878        {
879            cache.put(coords, chunk);
880        }
881    }
882
883    public static Chunk fetchDormantChunk(long coords, World world)
884    {
885        Cache<Long, Chunk> cache = dormantChunkCache.get(world);
886        if (cache == null)
887        {
888            return null;
889        }
890        Chunk chunk = cache.getIfPresent(coords);
891        if (chunk != null)
892        {
893            for (List<Entity> eList : chunk.entityLists)
894            {
895                for (Entity e: eList)
896                {
897                    e.resetEntityId();
898                }
899            }
900        }
901        return chunk;
902    }
903
904    static void captureConfig(File configDir)
905    {
906        cfgFile = new File(configDir,"forgeChunkLoading.cfg");
907        config = new Configuration(cfgFile, true);
908        try
909        {
910            config.load();
911        }
912        catch (Exception e)
913        {
914            File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
915            if (dest.exists())
916            {
917                dest.delete();
918            }
919            cfgFile.renameTo(dest);
920            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");
921        }
922        config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
923        Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
924        maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
925                    "in this file. This is the number of chunk loading requests a mod is allowed to make.";
926        defaultMaxCount = maxTicketCount.getInt(200);
927
928        Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
929        maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
930                    "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
931        defaultMaxChunks = maxChunks.getInt(25);
932
933        Property playerTicketCount = config.get("defaults", "playerTicketCount", 500);
934        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.";
935        playerTicketLength = playerTicketCount.getInt(500);
936
937        Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
938        dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
939                    "loading times. Specify the size of that cache here";
940        dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
941        FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
942
943        Property modOverridesEnabled = config.get("defaults", "enabled", true);
944        modOverridesEnabled.comment = "Are mod overrides enabled?";
945        overridesEnabled = modOverridesEnabled.getBoolean(true);
946
947        config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
948                "Copy this section and rename the with the modid for the mod you wish to override.\n" +
949                "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
950                "for that mod");
951
952        Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
953        sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
954        sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
955        sampleTC.comment = "Maximum chunks per ticket for the mod.";
956        for (String mod : config.categories.keySet())
957        {
958            if (mod.equals("Forge") || mod.equals("defaults"))
959            {
960                continue;
961            }
962            Property modTC = config.get(mod, "maximumTicketCount", 200);
963            Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
964        }
965    }
966
967
968    public static Map<String,Property> getConfigMapFor(Object mod)
969    {
970        ModContainer container = getContainer(mod);
971        if (container != null)
972        {
973            return config.getCategory(container.getModId()).getValues();
974        }
975
976        return null;
977    }
978
979    public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
980    {
981        ModContainer container = getContainer(mod);
982        if (container != null)
983        {
984            Map<String, Property> props = config.getCategory(container.getModId()).getValues();
985            props.put(propertyName, new Property(propertyName, value, type));
986        }
987    }
988}