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 ImmutableSetMultimap<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 }