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