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