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}