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 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 ticketCountAvaliableFor(String username) 609 { 610 return playerTicketLength - playerTickets.get(username).size(); 611 } 612 613 @Deprecated 614 public static Ticket requestPlayerTicket(Object mod, EntityPlayer player, World world, Type type) 615 { 616 return requestPlayerTicket(mod, player.getEntityName(), world, type); 617 } 618 619 public static Ticket requestPlayerTicket(Object mod, String player, World world, Type type) 620 { 621 ModContainer mc = getContainer(mod); 622 if (mc == null) 623 { 624 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 625 return null; 626 } 627 if (playerTickets.get(player).size()>playerTicketLength) 628 { 629 FMLLog.warning("Unable to assign further chunkloading tickets to player %s (on behalf of mod %s)", player, mc.getModId()); 630 return null; 631 } 632 Ticket ticket = new Ticket(mc.getModId(),type,world,player); 633 playerTickets.put(player, ticket); 634 tickets.get(world).put("Forge", ticket); 635 return ticket; 636 } 637 /** 638 * Request a chunkloading ticket of the appropriate type for the supplied mod 639 * 640 * @param mod The mod requesting a ticket 641 * @param world The world in which it is requesting the ticket 642 * @param type The type of ticket 643 * @return A ticket with which to register chunks for loading, or null if no further tickets are available 644 */ 645 public static Ticket requestTicket(Object mod, World world, Type type) 646 { 647 ModContainer container = getContainer(mod); 648 if (container == null) 649 { 650 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 651 return null; 652 } 653 String modId = container.getModId(); 654 if (!callbacks.containsKey(modId)) 655 { 656 FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId); 657 throw new RuntimeException("Invalid ticket request"); 658 } 659 660 int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount; 661 662 if (tickets.get(world).get(modId).size() >= allowedCount && !warnedMods.contains(modId)) 663 { 664 FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount); 665 warnedMods.add(modId); 666 return null; 667 } 668 Ticket ticket = new Ticket(modId, type, world); 669 tickets.get(world).put(modId, ticket); 670 671 return ticket; 672 } 673 674 /** 675 * 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. 676 * 677 * @param ticket The ticket to release 678 */ 679 public static void releaseTicket(Ticket ticket) 680 { 681 if (ticket == null) 682 { 683 return; 684 } 685 if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 686 { 687 return; 688 } 689 if (ticket.requestedChunks!=null) 690 { 691 for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks)) 692 { 693 unforceChunk(ticket, chunk); 694 } 695 } 696 if (ticket.isPlayerTicket()) 697 { 698 playerTickets.remove(ticket.player, ticket); 699 tickets.get(ticket.world).remove("Forge",ticket); 700 } 701 else 702 { 703 tickets.get(ticket.world).remove(ticket.modId, ticket); 704 } 705 } 706 707 /** 708 * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least 709 * recently registered chunk is unforced and may be unloaded. 710 * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering. 711 * 712 * @param ticket The ticket registering the chunk 713 * @param chunk The chunk to force 714 */ 715 public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk) 716 { 717 if (ticket == null || chunk == null) 718 { 719 return; 720 } 721 if (ticket.ticketType == Type.ENTITY && ticket.entity == null) 722 { 723 throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity"); 724 } 725 if (ticket.isPlayerTicket() ? !playerTickets.containsValue(ticket) : !tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 726 { 727 FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId); 728 return; 729 } 730 ticket.requestedChunks.add(chunk); 731 MinecraftForge.EVENT_BUS.post(new ForceChunkEvent(ticket, chunk)); 732 733 ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>builder().putAll(forcedChunks.get(ticket.world)).put(chunk, ticket).build(); 734 forcedChunks.put(ticket.world, newMap); 735 if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth) 736 { 737 ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next(); 738 unforceChunk(ticket,removed); 739 } 740 } 741 742 /** 743 * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list 744 * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks 745 * in the ticket list 746 * 747 * @param ticket The ticket holding the chunk list 748 * @param chunk The chunk you wish to push to the end (so that it would be unloaded last) 749 */ 750 public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk) 751 { 752 if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk)) 753 { 754 return; 755 } 756 ticket.requestedChunks.remove(chunk); 757 ticket.requestedChunks.add(chunk); 758 } 759 /** 760 * Unforce the supplied chunk, allowing it to be unloaded and stop ticking. 761 * 762 * @param ticket The ticket holding the chunk 763 * @param chunk The chunk to unforce 764 */ 765 public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk) 766 { 767 if (ticket == null || chunk == null) 768 { 769 return; 770 } 771 ticket.requestedChunks.remove(chunk); 772 MinecraftForge.EVENT_BUS.post(new UnforceChunkEvent(ticket, chunk)); 773 LinkedHashMultimap<ChunkCoordIntPair, Ticket> copy = LinkedHashMultimap.create(forcedChunks.get(ticket.world)); 774 copy.remove(chunk, ticket); 775 ImmutableSetMultimap<ChunkCoordIntPair, Ticket> newMap = ImmutableSetMultimap.copyOf(copy); 776 forcedChunks.put(ticket.world,newMap); 777 } 778 779 static void loadConfiguration() 780 { 781 for (String mod : config.categories.keySet()) 782 { 783 if (mod.equals("Forge") || mod.equals("defaults")) 784 { 785 continue; 786 } 787 Property modTC = config.get(mod, "maximumTicketCount", 200); 788 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25); 789 ticketConstraints.put(mod, modTC.getInt(200)); 790 chunkConstraints.put(mod, modCPT.getInt(25)); 791 } 792 config.save(); 793 } 794 795 /** 796 * The list of persistent chunks in the world. This set is immutable. 797 * @param world 798 * @return 799 */ 800 public static ImmutableSetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world) 801 { 802 return forcedChunks.containsKey(world) ? forcedChunks.get(world) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of(); 803 } 804 805 static void saveWorld(World world) 806 { 807 // only persist persistent worlds 808 if (!(world instanceof WorldServer)) { return; } 809 WorldServer worldServer = (WorldServer) world; 810 File chunkDir = worldServer.getChunkSaveLocation(); 811 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat"); 812 813 NBTTagCompound forcedChunkData = new NBTTagCompound(); 814 NBTTagList ticketList = new NBTTagList(); 815 forcedChunkData.setTag("TicketList", ticketList); 816 817 Multimap<String, Ticket> ticketSet = tickets.get(worldServer); 818 for (String modId : ticketSet.keySet()) 819 { 820 NBTTagCompound ticketHolder = new NBTTagCompound(); 821 ticketList.appendTag(ticketHolder); 822 823 ticketHolder.setString("Owner", modId); 824 NBTTagList tickets = new NBTTagList(); 825 ticketHolder.setTag("Tickets", tickets); 826 827 for (Ticket tick : ticketSet.get(modId)) 828 { 829 NBTTagCompound ticket = new NBTTagCompound(); 830 ticket.setByte("Type", (byte) tick.ticketType.ordinal()); 831 ticket.setByte("ChunkListDepth", (byte) tick.maxDepth); 832 if (tick.isPlayerTicket()) 833 { 834 ticket.setString("ModId", tick.modId); 835 ticket.setString("Player", tick.player); 836 } 837 if (tick.modData != null) 838 { 839 ticket.setCompoundTag("ModData", tick.modData); 840 } 841 if (tick.ticketType == Type.ENTITY && tick.entity != null && tick.entity.addEntityID(new NBTTagCompound())) 842 { 843 ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX)); 844 ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ)); 845 ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits()); 846 ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits()); 847 tickets.appendTag(ticket); 848 } 849 else if (tick.ticketType != Type.ENTITY) 850 { 851 tickets.appendTag(ticket); 852 } 853 } 854 } 855 try 856 { 857 CompressedStreamTools.write(forcedChunkData, chunkLoaderData); 858 } 859 catch (IOException e) 860 { 861 FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath()); 862 return; 863 } 864 } 865 866 static void loadEntity(Entity entity) 867 { 868 UUID id = entity.getPersistentID(); 869 Ticket tick = pendingEntities.get(id); 870 if (tick != null) 871 { 872 tick.bindEntity(entity); 873 pendingEntities.remove(id); 874 } 875 } 876 877 public static void putDormantChunk(long coords, Chunk chunk) 878 { 879 Cache<Long, Chunk> cache = dormantChunkCache.get(chunk.worldObj); 880 if (cache != null) 881 { 882 cache.put(coords, chunk); 883 } 884 } 885 886 public static Chunk fetchDormantChunk(long coords, World world) 887 { 888 Cache<Long, Chunk> cache = dormantChunkCache.get(world); 889 return cache == null ? null : cache.getIfPresent(coords); 890 } 891 892 static void captureConfig(File configDir) 893 { 894 cfgFile = new File(configDir,"forgeChunkLoading.cfg"); 895 config = new Configuration(cfgFile, true); 896 try 897 { 898 config.load(); 899 } 900 catch (Exception e) 901 { 902 File dest = new File(cfgFile.getParentFile(),"forgeChunkLoading.cfg.bak"); 903 if (dest.exists()) 904 { 905 dest.delete(); 906 } 907 cfgFile.renameTo(dest); 908 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"); 909 } 910 config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control"); 911 Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200); 912 maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" + 913 "in this file. This is the number of chunk loading requests a mod is allowed to make."; 914 defaultMaxCount = maxTicketCount.getInt(200); 915 916 Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25); 917 maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" + 918 "for a mod without an override. This is the maximum number of chunks a single ticket can force."; 919 defaultMaxChunks = maxChunks.getInt(25); 920 921 Property playerTicketCount = config.get("defaults", "playerTicketCount", 500); 922 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."; 923 playerTicketLength = playerTicketCount.getInt(500); 924 925 Property dormantChunkCacheSizeProperty = config.get("defaults", "dormantChunkCacheSize", 0); 926 dormantChunkCacheSizeProperty.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" + 927 "loading times. Specify the size of that cache here"; 928 dormantChunkCacheSize = dormantChunkCacheSizeProperty.getInt(0); 929 FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSizeProperty.getInt(0)); 930 931 Property modOverridesEnabled = config.get("defaults", "enabled", true); 932 modOverridesEnabled.comment = "Are mod overrides enabled?"; 933 overridesEnabled = modOverridesEnabled.getBoolean(true); 934 935 config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" + 936 "Copy this section and rename the with the modid for the mod you wish to override.\n" + 937 "A value of zero in either entry effectively disables any chunkloading capabilities\n" + 938 "for that mod"); 939 940 Property sampleTC = config.get("Forge", "maximumTicketCount", 200); 941 sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities."; 942 sampleTC = config.get("Forge", "maximumChunksPerTicket", 25); 943 sampleTC.comment = "Maximum chunks per ticket for the mod."; 944 for (String mod : config.categories.keySet()) 945 { 946 if (mod.equals("Forge") || mod.equals("defaults")) 947 { 948 continue; 949 } 950 Property modTC = config.get(mod, "maximumTicketCount", 200); 951 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25); 952 } 953 } 954 955 956 public static Map<String,Property> getConfigMapFor(Object mod) 957 { 958 ModContainer container = getContainer(mod); 959 if (container != null) 960 { 961 return config.getCategory(container.getModId()).getValues(); 962 } 963 964 return null; 965 } 966 967 public static void addConfigProperty(Object mod, String propertyName, String value, Property.Type type) 968 { 969 ModContainer container = getContainer(mod); 970 if (container != null) 971 { 972 Map<String, Property> props = config.getCategory(container.getModId()).getValues(); 973 props.put(propertyName, new Property(propertyName, value, type)); 974 } 975 } 976 }