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