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