001 package net.minecraftforge.common;
002
003 import java.io.DataInputStream;
004 import java.io.File;
005 import java.io.FileInputStream;
006 import java.io.IOException;
007 import java.util.HashSet;
008 import java.util.LinkedHashSet;
009 import java.util.LinkedList;
010 import java.util.List;
011 import java.util.Map;
012 import java.util.Set;
013 import java.util.UUID;
014 import java.util.logging.Level;
015
016 import com.google.common.base.Supplier;
017 import com.google.common.base.Suppliers;
018 import com.google.common.cache.Cache;
019 import com.google.common.cache.CacheBuilder;
020 import com.google.common.collect.ArrayListMultimap;
021 import com.google.common.collect.BiMap;
022 import com.google.common.collect.ForwardingSet;
023 import com.google.common.collect.HashBiMap;
024 import com.google.common.collect.HashMultimap;
025 import com.google.common.collect.ImmutableList;
026 import com.google.common.collect.ImmutableListMultimap;
027 import com.google.common.collect.ImmutableSet;
028 import com.google.common.collect.ImmutableSetMultimap;
029 import com.google.common.collect.LinkedHashMultimap;
030 import com.google.common.collect.ListMultimap;
031 import com.google.common.collect.Lists;
032 import com.google.common.collect.MapMaker;
033 import com.google.common.collect.Maps;
034 import com.google.common.collect.Multimap;
035 import com.google.common.collect.Multimaps;
036 import com.google.common.collect.Multiset;
037 import com.google.common.collect.SetMultimap;
038 import com.google.common.collect.Sets;
039 import com.google.common.collect.TreeMultiset;
040
041 import cpw.mods.fml.common.FMLLog;
042 import cpw.mods.fml.common.Loader;
043 import cpw.mods.fml.common.ModContainer;
044
045 import net.minecraft.server.MinecraftServer;
046 import net.minecraft.world.chunk.Chunk;
047 import net.minecraft.world.ChunkCoordIntPair;
048 import net.minecraft.nbt.CompressedStreamTools;
049 import net.minecraft.entity.Entity;
050 import net.minecraft.entity.player.EntityPlayer;
051 import net.minecraft.util.MathHelper;
052 import net.minecraft.nbt.NBTBase;
053 import net.minecraft.nbt.NBTTagCompound;
054 import net.minecraft.nbt.NBTTagList;
055 import net.minecraft.world.World;
056 import net.minecraft.world.WorldServer;
057 import net.minecraftforge.common.ForgeChunkManager.Ticket;
058 import 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 */
077 public 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
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
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 }