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.cache.Cache;
017 import com.google.common.cache.CacheBuilder;
018 import com.google.common.collect.ArrayListMultimap;
019 import com.google.common.collect.BiMap;
020 import com.google.common.collect.HashBiMap;
021 import com.google.common.collect.HashMultimap;
022 import com.google.common.collect.ImmutableList;
023 import com.google.common.collect.ImmutableSet;
024 import com.google.common.collect.ImmutableSetMultimap;
025 import com.google.common.collect.LinkedHashMultimap;
026 import com.google.common.collect.ListMultimap;
027 import com.google.common.collect.Lists;
028 import com.google.common.collect.MapMaker;
029 import com.google.common.collect.Maps;
030 import com.google.common.collect.Multimap;
031 import com.google.common.collect.Multiset;
032 import com.google.common.collect.SetMultimap;
033 import com.google.common.collect.Sets;
034 import com.google.common.collect.TreeMultiset;
035
036 import cpw.mods.fml.common.FMLLog;
037 import cpw.mods.fml.common.Loader;
038 import cpw.mods.fml.common.ModContainer;
039
040 import net.minecraft.src.Chunk;
041 import net.minecraft.src.ChunkCoordIntPair;
042 import net.minecraft.src.CompressedStreamTools;
043 import net.minecraft.src.Entity;
044 import net.minecraft.src.EntityPlayer;
045 import net.minecraft.src.MathHelper;
046 import net.minecraft.src.NBTBase;
047 import net.minecraft.src.NBTTagCompound;
048 import net.minecraft.src.NBTTagList;
049 import net.minecraft.src.World;
050 import net.minecraft.src.WorldServer;
051 import net.minecraftforge.common.ForgeChunkManager.Ticket;
052
053 /**
054 * Manages chunkloading for mods.
055 *
056 * The basic principle is a ticket based system.
057 * 1. Mods register a callback {@link #setForcedChunkLoadingCallback(Object, LoadingCallback)}
058 * 2. Mods ask for a ticket {@link #requestTicket(Object, World, Type)} and then hold on to that ticket.
059 * 3. Mods request chunks to stay loaded {@link #forceChunk(Ticket, ChunkCoordIntPair)} or remove chunks from force loading {@link #unforceChunk(Ticket, ChunkCoordIntPair)}.
060 * 4. When a world unloads, the tickets associated with that world are saved by the chunk manager.
061 * 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
062 * chunks to stay loaded (and maybe take other actions).
063 *
064 * The chunkloading is configurable at runtime. The file "config/forgeChunkLoading.cfg" contains both default configuration for chunkloading, and a sample individual mod
065 * specific override section.
066 *
067 * @author cpw
068 *
069 */
070 public class ForgeChunkManager
071 {
072 private static int defaultMaxCount;
073 private static int defaultMaxChunks;
074 private static boolean overridesEnabled;
075
076 private static Map<World, Multimap<String, Ticket>> tickets = new MapMaker().weakKeys().makeMap();
077 private static Map<String, Integer> ticketConstraints = Maps.newHashMap();
078 private static Map<String, Integer> chunkConstraints = Maps.newHashMap();
079
080 private static SetMultimap<String, Ticket> playerTickets = HashMultimap.create();
081
082 private static Map<String, LoadingCallback> callbacks = Maps.newHashMap();
083
084 private static Map<World, ImmutableSetMultimap<ChunkCoordIntPair,Ticket>> forcedChunks = new MapMaker().weakKeys().makeMap();
085 private static BiMap<UUID,Ticket> pendingEntities = HashBiMap.create();
086
087 private static Map<World,Cache<Long, Chunk>> dormantChunkCache = new MapMaker().weakKeys().makeMap();
088
089 private static File cfgFile;
090 private static Configuration config;
091 private static int playerTicketLength;
092 private static int dormantChunkCacheSize;
093 /**
094 * All mods requiring chunkloading need to implement this to handle the
095 * re-registration of chunk tickets at world loading time
096 *
097 * @author cpw
098 *
099 */
100 public interface LoadingCallback
101 {
102 /**
103 * Called back when tickets are loaded from the world to allow the
104 * mod to re-register the chunks associated with those tickets. The list supplied
105 * here is truncated to length prior to use. Tickets unwanted by the
106 * mod must be disposed of manually unless the mod is an OrderedLoadingCallback instance
107 * in which case, they will have been disposed of by the earlier callback.
108 *
109 * @param tickets The tickets to re-register. The list is immutable and cannot be manipulated directly. Copy it first.
110 * @param world the world
111 */
112 public void ticketsLoaded(List<Ticket> tickets, World world);
113 }
114
115 /**
116 * This is a special LoadingCallback that can be implemented as well as the
117 * LoadingCallback to provide access to additional behaviour.
118 * Specifically, this callback will fire prior to Forge dropping excess
119 * tickets. Tickets in the returned list are presumed ordered and excess will
120 * be truncated from the returned list.
121 * This allows the mod to control not only if they actually <em>want</em> a ticket but
122 * also their preferred ticket ordering.
123 *
124 * @author cpw
125 *
126 */
127 public interface OrderedLoadingCallback extends LoadingCallback
128 {
129 /**
130 * Called back when tickets are loaded from the world to allow the
131 * mod to decide if it wants the ticket still, and prioritise overflow
132 * based on the ticket count.
133 * WARNING: You cannot force chunks in this callback, it is strictly for allowing the mod
134 * to be more selective in which tickets it wishes to preserve in an overflow situation
135 *
136 * @param tickets The tickets that you will want to select from. The list is immutable and cannot be manipulated directly. Copy it first.
137 * @param world The world
138 * @param maxTicketCount The maximum number of tickets that will be allowed.
139 * @return A list of the tickets this mod wishes to continue using. This list will be truncated
140 * to "maxTicketCount" size after the call returns and then offered to the other callback
141 * method
142 */
143 public List<Ticket> ticketsLoaded(List<Ticket> tickets, World world, int maxTicketCount);
144 }
145 public enum Type
146 {
147
148 /**
149 * For non-entity registrations
150 */
151 NORMAL,
152 /**
153 * For entity registrations
154 */
155 ENTITY
156 }
157 public static class Ticket
158 {
159 private String modId;
160 private Type ticketType;
161 private LinkedHashSet<ChunkCoordIntPair> requestedChunks;
162 private NBTTagCompound modData;
163 private World world;
164 private int maxDepth;
165 private String entityClazz;
166 private int entityChunkX;
167 private int entityChunkZ;
168 private Entity entity;
169 private String player;
170
171 Ticket(String modId, Type type, World world)
172 {
173 this.modId = modId;
174 this.ticketType = type;
175 this.world = world;
176 this.maxDepth = getMaxChunkDepthFor(modId);
177 this.requestedChunks = Sets.newLinkedHashSet();
178 }
179
180 Ticket(String modId, Type type, World world, EntityPlayer player)
181 {
182 this(modId, type, world);
183 if (player != null)
184 {
185 this.player = player.getEntityName();
186 }
187 else
188 {
189 FMLLog.log(Level.SEVERE, "Attempt to create a player ticket without a valid player");
190 throw new RuntimeException();
191 }
192 }
193 /**
194 * 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,
195 * the least recently forced chunk, by original registration time, is removed from the forced chunk list.
196 *
197 * @param depth The new depth to set
198 */
199 public void setChunkListDepth(int depth)
200 {
201 if (depth > getMaxChunkDepthFor(modId) || (depth <= 0 && getMaxChunkDepthFor(modId) > 0))
202 {
203 FMLLog.warning("The mod %s tried to modify the chunk ticket depth to: %d, its allowed maximum is: %d", modId, depth, getMaxChunkDepthFor(modId));
204 }
205 else
206 {
207 this.maxDepth = depth;
208 }
209 }
210
211 /**
212 * Gets the current max depth for this ticket.
213 * Should be the same as getMaxChunkListDepth()
214 * unless setChunkListDepth has been called.
215 *
216 * @return Current max depth
217 */
218 public int getChunkListDepth()
219 {
220 return maxDepth;
221 }
222
223 /**
224 * Get the maximum chunk depth size
225 *
226 * @return The maximum chunk depth size
227 */
228 public int getMaxChunkListDepth()
229 {
230 return getMaxChunkDepthFor(modId);
231 }
232
233 /**
234 * Bind the entity to the ticket for {@link Type#ENTITY} type tickets. Other types will throw a runtime exception.
235 *
236 * @param entity The entity to bind
237 */
238 public void bindEntity(Entity entity)
239 {
240 if (ticketType!=Type.ENTITY)
241 {
242 throw new RuntimeException("Cannot bind an entity to a non-entity ticket");
243 }
244 this.entity = entity;
245 }
246
247 /**
248 * Retrieve the {@link NBTTagCompound} that stores mod specific data for the chunk ticket.
249 * Example data to store would be a TileEntity or Block location. This is persisted with the ticket and
250 * provided to the {@link LoadingCallback} for the mod. It is recommended to use this to recover
251 * useful state information for the forced chunks.
252 *
253 * @return The custom compound tag for mods to store additional chunkloading data
254 */
255 public NBTTagCompound getModData()
256 {
257 if (this.modData == null)
258 {
259 this.modData = new NBTTagCompound();
260 }
261 return modData;
262 }
263
264 /**
265 * Get the entity associated with this {@link Type#ENTITY} type ticket
266 * @return
267 */
268 public Entity getEntity()
269 {
270 return entity;
271 }
272
273 /**
274 * Is this a player associated ticket rather than a mod associated ticket?
275 */
276 public boolean isPlayerTicket()
277 {
278 return player != null;
279 }
280
281 /**
282 * Get the player associated with this ticket
283 */
284 public String getPlayerName()
285 {
286 return player;
287 }
288
289 /**
290 * Get the associated mod id
291 */
292 public String getModId()
293 {
294 return modId;
295 }
296
297 /**
298 * Gets the ticket type
299 */
300 public Type getType()
301 {
302 return ticketType;
303 }
304
305 /**
306 * Gets a list of requested chunks for this ticket.
307 */
308 public ImmutableSet getChunkList()
309 {
310 return ImmutableSet.copyOf(requestedChunks);
311 }
312 }
313
314 static void loadWorld(World world)
315 {
316 ArrayListMultimap<String, Ticket> newTickets = ArrayListMultimap.<String, Ticket>create();
317 tickets.put(world, newTickets);
318
319 forcedChunks.put(world, ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of());
320
321 if (!(world instanceof WorldServer))
322 {
323 return;
324 }
325
326 dormantChunkCache.put(world, CacheBuilder.newBuilder().maximumSize(dormantChunkCacheSize).<Long, Chunk>build());
327 WorldServer worldServer = (WorldServer) world;
328 File chunkDir = worldServer.getChunkSaveLocation();
329 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
330
331 if (chunkLoaderData.exists() && chunkLoaderData.isFile())
332 {
333 ArrayListMultimap<String, Ticket> loadedTickets = ArrayListMultimap.<String, Ticket>create();
334 ArrayListMultimap<String, Ticket> playerLoadedTickets = ArrayListMultimap.<String, Ticket>create();
335 NBTTagCompound forcedChunkData;
336 try
337 {
338 forcedChunkData = CompressedStreamTools.read(chunkLoaderData);
339 }
340 catch (IOException e)
341 {
342 FMLLog.log(Level.WARNING, e, "Unable to read forced chunk data at %s - it will be ignored", chunkLoaderData.getAbsolutePath());
343 return;
344 }
345 NBTTagList ticketList = forcedChunkData.getTagList("TicketList");
346 for (int i = 0; i < ticketList.tagCount(); i++)
347 {
348 NBTTagCompound ticketHolder = (NBTTagCompound) ticketList.tagAt(i);
349 String modId = ticketHolder.getString("Owner");
350 boolean isPlayer = "Forge".equals(modId);
351
352 if (!isPlayer && !Loader.isModLoaded(modId))
353 {
354 FMLLog.warning("Found chunkloading data for mod %s which is currently not available or active - it will be removed from the world save", modId);
355 continue;
356 }
357
358 if (!isPlayer && !callbacks.containsKey(modId))
359 {
360 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);
361 continue;
362 }
363
364 NBTTagList tickets = ticketHolder.getTagList("Tickets");
365 for (int j = 0; j < tickets.tagCount(); j++)
366 {
367 NBTTagCompound ticket = (NBTTagCompound) tickets.tagAt(j);
368 modId = ticket.hasKey("ModId") ? ticket.getString("ModId") : modId;
369 Type type = Type.values()[ticket.getByte("Type")];
370 byte ticketChunkDepth = ticket.getByte("ChunkListDepth");
371 Ticket tick = new Ticket(modId, type, world);
372 if (ticket.hasKey("ModData"))
373 {
374 tick.modData = ticket.getCompoundTag("ModData");
375 }
376 if (ticket.hasKey("Player"))
377 {
378 tick.player = ticket.getString("Player");
379 playerLoadedTickets.put(tick.modId, tick);
380 playerTickets.put(tick.player, tick);
381 }
382 else
383 {
384 loadedTickets.put(modId, tick);
385 }
386 if (type == Type.ENTITY)
387 {
388 tick.entityChunkX = ticket.getInteger("chunkX");
389 tick.entityChunkZ = ticket.getInteger("chunkZ");
390 UUID uuid = new UUID(ticket.getLong("PersistentIDMSB"), ticket.getLong("PersistentIDLSB"));
391 // add the ticket to the "pending entity" list
392 pendingEntities.put(uuid, tick);
393 }
394 }
395 }
396
397 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
398 {
399 if (tick.ticketType == Type.ENTITY && tick.entity == null)
400 {
401 // force the world to load the entity's chunk
402 // the load will come back through the loadEntity method and attach the entity
403 // to the ticket
404 world.getChunkFromChunkCoords(tick.entityChunkX, tick.entityChunkZ);
405 }
406 }
407 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values()))
408 {
409 if (tick.ticketType == Type.ENTITY && tick.entity == null)
410 {
411 FMLLog.warning("Failed to load persistent chunkloading entity %s from store.", pendingEntities.inverse().get(tick));
412 loadedTickets.remove(tick.modId, tick);
413 }
414 }
415 pendingEntities.clear();
416 // send callbacks
417 for (String modId : loadedTickets.keySet())
418 {
419 LoadingCallback loadingCallback = callbacks.get(modId);
420 int maxTicketLength = getMaxTicketLengthFor(modId);
421 List<Ticket> tickets = loadedTickets.get(modId);
422 if (loadingCallback instanceof OrderedLoadingCallback)
423 {
424 OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback;
425 tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength);
426 }
427 if (tickets.size() > maxTicketLength)
428 {
429 FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size());
430 tickets.subList(maxTicketLength, tickets.size()).clear();
431 }
432 ForgeChunkManager.tickets.get(world).putAll(modId, tickets);
433 loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
434 }
435 for (String modId : playerLoadedTickets.keySet())
436 {
437 LoadingCallback loadingCallback = callbacks.get(modId);
438 List<Ticket> tickets = playerLoadedTickets.get(modId);
439 ForgeChunkManager.tickets.get(world).putAll("Forge", tickets);
440 loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world);
441 }
442 }
443 }
444
445 /**
446 * Set a chunkloading callback for the supplied mod object
447 *
448 * @param mod The mod instance registering the callback
449 * @param callback The code to call back when forced chunks are loaded
450 */
451 public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback)
452 {
453 ModContainer container = getContainer(mod);
454 if (container == null)
455 {
456 FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
457 return;
458 }
459
460 callbacks.put(container.getModId(), callback);
461 }
462
463 /**
464 * Discover the available tickets for the mod in the world
465 *
466 * @param mod The mod that will own the tickets
467 * @param world The world
468 * @return The count of tickets left for the mod in the supplied world
469 */
470 public static int ticketCountAvailableFor(Object mod, World world)
471 {
472 ModContainer container = getContainer(mod);
473 if (container!=null)
474 {
475 String modId = container.getModId();
476 int allowedCount = getMaxTicketLengthFor(modId);
477 return allowedCount - tickets.get(world).get(modId).size();
478 }
479 else
480 {
481 return 0;
482 }
483 }
484
485 private static ModContainer getContainer(Object mod)
486 {
487 ModContainer container = Loader.instance().getModObjectList().inverse().get(mod);
488 return container;
489 }
490
491 private static int getMaxTicketLengthFor(String modId)
492 {
493 int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount;
494 return allowedCount;
495 }
496
497 private static int getMaxChunkDepthFor(String modId)
498 {
499 int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks;
500 return allowedCount;
501 }
502
503 public static Ticket requestPlayerTicket(Object mod, EntityPlayer player, World world, Type type)
504 {
505 ModContainer mc = getContainer(mod);
506 if (mc == null)
507 {
508 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
509 return null;
510 }
511 if (playerTickets.get(player.getEntityName()).size()>playerTicketLength)
512 {
513 FMLLog.warning("Unable to assign further chunkloading tickets to player %s (on behalf of mod %s)", player.getEntityName(), mc.getModId());
514 return null;
515 }
516 Ticket ticket = new Ticket(mc.getModId(),type,world,player);
517 playerTickets.put(player.getEntityName(), ticket);
518 tickets.get(world).put("Forge", ticket);
519 return ticket;
520 }
521 /**
522 * Request a chunkloading ticket of the appropriate type for the supplied mod
523 *
524 * @param mod The mod requesting a ticket
525 * @param world The world in which it is requesting the ticket
526 * @param type The type of ticket
527 * @return A ticket with which to register chunks for loading, or null if no further tickets are available
528 */
529 public static Ticket requestTicket(Object mod, World world, Type type)
530 {
531 ModContainer container = getContainer(mod);
532 if (container == null)
533 {
534 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod));
535 return null;
536 }
537 String modId = container.getModId();
538 if (!callbacks.containsKey(modId))
539 {
540 FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId);
541 throw new RuntimeException("Invalid ticket request");
542 }
543
544 int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount;
545
546 if (tickets.get(world).get(modId).size() >= allowedCount)
547 {
548 FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount);
549 return null;
550 }
551 Ticket ticket = new Ticket(modId, type, world);
552 tickets.get(world).put(modId, ticket);
553
554 return ticket;
555 }
556
557 /**
558 * 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.
559 *
560 * @param ticket The ticket to release
561 */
562 public static void releaseTicket(Ticket ticket)
563 {
564 if (ticket == null)
565 {
566 return;
567 }
568 if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
569 {
570 return;
571 }
572 if (ticket.requestedChunks!=null)
573 {
574 for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks))
575 {
576 unforceChunk(ticket, chunk);
577 }
578 }
579 if (ticket.isPlayerTicket())
580 {
581 playerTickets.remove(ticket.player, ticket);
582 tickets.get(ticket.world).remove("Forge",ticket);
583 }
584 else
585 {
586 tickets.get(ticket.world).remove(ticket.modId, ticket);
587 }
588 }
589
590 /**
591 * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least
592 * recently registered chunk is unforced and may be unloaded.
593 * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering.
594 *
595 * @param ticket The ticket registering the chunk
596 * @param chunk The chunk to force
597 */
598 public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk)
599 {
600 if (ticket == null || chunk == null)
601 {
602 return;
603 }
604 if (ticket.ticketType == Type.ENTITY && ticket.entity == null)
605 {
606 throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity");
607 }
608 if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket))
609 {
610 FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId);
611 return;
612 }
613 ticket.requestedChunks.add(chunk);
614 ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build();
615 forcedChunks.put(ticket.world, newMap);
616 if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth)
617 {
618 ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next();
619 unforceChunk(ticket,removed);
620 }
621 }
622
623 /**
624 * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list
625 * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks
626 * in the ticket list
627 *
628 * @param ticket The ticket holding the chunk list
629 * @param chunk The chunk you wish to push to the end (so that it would be unloaded last)
630 */
631 public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk)
632 {
633 if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk))
634 {
635 return;
636 }
637 ticket.requestedChunks.remove(chunk);
638 ticket.requestedChunks.add(chunk);
639 }
640 /**
641 * Unforce the supplied chunk, allowing it to be unloaded and stop ticking.
642 *
643 * @param ticket The ticket holding the chunk
644 * @param chunk The chunk to unforce
645 */
646 public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk)
647 {
648 if (ticket == null || chunk == null)
649 {
650 return;
651 }
652 ticket.requestedChunks.remove(chunk);
653 LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world));
654 copy.remove(chunk, ticket);
655 ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy);
656 forcedChunks.put(ticket.world,newMap);
657 }
658
659 static void loadConfiguration()
660 {
661 for (String mod : config.categories.keySet())
662 {
663 if (mod.equals("Forge") || mod.equals("defaults"))
664 {
665 continue;
666 }
667 Property modTC = config.get(mod, "maximumTicketCount", 200);
668 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
669 ticketConstraints.put(mod, modTC.getInt(200));
670 chunkConstraints.put(mod, modCPT.getInt(25));
671 }
672 config.save();
673 }
674
675 /**
676 * The list of persistent chunks in the world. This set is immutable.
677 * @param world
678 * @return
679 */
680 public static SetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world)
681 {
682 return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of();
683 }
684
685 static void saveWorld(World world)
686 {
687 // only persist persistent worlds
688 if (!(world instanceof WorldServer)) { return; }
689 WorldServer worldServer = (WorldServer) world;
690 File chunkDir = worldServer.getChunkSaveLocation();
691 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat");
692
693 NBTTagCompound forcedChunkData = new NBTTagCompound();
694 NBTTagList ticketList = new NBTTagList();
695 forcedChunkData.setTag("TicketList", ticketList);
696
697 Multimap<String, Ticket> ticketSet = tickets.get(worldServer);
698 for (String modId : ticketSet.keySet())
699 {
700 NBTTagCompound ticketHolder = new NBTTagCompound();
701 ticketList.appendTag(ticketHolder);
702
703 ticketHolder.setString("Owner", modId);
704 NBTTagList tickets = new NBTTagList();
705 ticketHolder.setTag("Tickets", tickets);
706
707 for (Ticket tick : ticketSet.get(modId))
708 {
709 NBTTagCompound ticket = new NBTTagCompound();
710 ticket.setByte("Type", (byte) tick.ticketType.ordinal());
711 ticket.setByte("ChunkListDepth", (byte) tick.maxDepth);
712 if (tick.isPlayerTicket())
713 {
714 ticket.setString("ModId", tick.modId);
715 ticket.setString("Player", tick.player);
716 }
717 if (tick.modData != null)
718 {
719 ticket.setCompoundTag("ModData", tick.modData);
720 }
721 if (tick.ticketType == Type.ENTITY && tick.entity != null)
722 {
723 ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX));
724 ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ));
725 ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits());
726 ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits());
727 tickets.appendTag(ticket);
728 }
729 else if (tick.ticketType != Type.ENTITY)
730 {
731 tickets.appendTag(ticket);
732 }
733 }
734 }
735 try
736 {
737 CompressedStreamTools.write(forcedChunkData, chunkLoaderData);
738 }
739 catch (IOException e)
740 {
741 FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath());
742 return;
743 }
744 }
745
746 static void loadEntity(Entity entity)
747 {
748 UUID id = entity.getPersistentID();
749 Ticket tick = pendingEntities.get(id);
750 if (tick != null)
751 {
752 tick.bindEntity(entity);
753 pendingEntities.remove(id);
754 }
755 }
756
757 public static void putDormantChunk(long coords, Chunk chunk)
758 {
759 Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj);
760 if (cache != null)
761 {
762 cache.put(coords, chunk);
763 }
764 }
765
766 public static Chunk fetchDormantChunk(long coords, World world)
767 {
768 Cache<Long, Chunk> cache = dormantChunkCache.get(world);
769 return cache == null ? null : cache.getIfPresent(coords);
770 }
771
772 static void captureConfig(File configDir)
773 {
774 cfgFile = new File(configDir,"forgeChunkLoading.cfg");
775 config = new Configuration(cfgFile, true);
776 config.categories.clear();
777 try
778 {
779 config.load();
780 }
781 catch (Exception e)
782 {
783 File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak");
784 if (dest.exists())
785 {
786 dest.delete();
787 }
788 cfgFile.renameTo(dest);
789 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");
790 }
791 config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control");
792 Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200);
793 maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" +
794 "in this file. This is the number of chunk loading requests a mod is allowed to make.";
795 defaultMaxCount = maxTicketCount.getInt(200);
796
797 Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25);
798 maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" +
799 "for a mod without an override. This is the maximum number of chunks a single ticket can force.";
800 defaultMaxChunks = maxChunks.getInt(25);
801
802 Property playerTicketCount = config.get("defaults", "playetTicketCount", 500);
803 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.";
804 playerTicketLength = playerTicketCount.getInt(500);
805
806 Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0);
807 dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" +
808 "loading times. Specify the size of that cache here";
809 dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0);
810 FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0));
811
812 Property modOverridesEnabled = config.get("defaults", "enabled", true);
813 modOverridesEnabled.comment = "Are mod overrides enabled?";
814 overridesEnabled = modOverridesEnabled.getBoolean(true);
815
816 config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" +
817 "Copy this section and rename the with the modid for the mod you wish to override.\n" +
818 "A value of zero in either entry effectively disables any chunkloading capabilities\n" +
819 "for that mod");
820
821 Property sampleTC = config.get("Forge", "maximumTicketCount", 200);
822 sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities.";
823 sampleTC = config.get("Forge", "maximumChunksPerTicket", 25);
824 sampleTC.comment = "Maximum chunks per ticket for the mod.";
825 for (String mod : config.categories.keySet())
826 {
827 if (mod.equals("Forge") || mod.equals("defaults"))
828 {
829 continue;
830 }
831 Property modTC = config.get(mod, "maximumTicketCount", 200);
832 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25);
833 }
834 }
835
836
837 public static Map<String,Property> getConfigMapFor(Object mod)
838 {
839 ModContainer container = getContainer(mod);
840 if (container != null)
841 {
842 Map<String, Property> map = config.categories.get(container.getModId());
843 if (map == null)
844 {
845 map = Maps.newHashMap();
846 config.categories.put(container.getModId(), map);
847 }
848 return map;
849 }
850
851 return null;
852 }
853
854 public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type)
855 {
856 ModContainer container = getContainer(mod);
857 if (container != null)
858 {
859 Map<String, Property> props = config.categories.get(container.getModId());
860 props.put(propertyName, new Property(propertyName, value, type));
861 }
862 }
863 }