001 package net.minecraftforge.common; 002 003 import java.io.DataInputStream; 004 import java.io.File; 005 import java.io.FileInputStream; 006 import java.io.IOException; 007 import java.util.HashSet; 008 import java.util.LinkedHashSet; 009 import java.util.LinkedList; 010 import java.util.List; 011 import java.util.Map; 012 import java.util.Set; 013 import java.util.UUID; 014 import java.util.logging.Level; 015 016 import com.google.common.cache.Cache; 017 import com.google.common.cache.CacheBuilder; 018 import com.google.common.collect.ArrayListMultimap; 019 import com.google.common.collect.BiMap; 020 import com.google.common.collect.HashBiMap; 021 import com.google.common.collect.ImmutableList; 022 import com.google.common.collect.ImmutableSet; 023 import com.google.common.collect.ImmutableSetMultimap; 024 import com.google.common.collect.LinkedHashMultimap; 025 import com.google.common.collect.ListMultimap; 026 import com.google.common.collect.Lists; 027 import com.google.common.collect.Maps; 028 import com.google.common.collect.Multimap; 029 import com.google.common.collect.Multiset; 030 import com.google.common.collect.SetMultimap; 031 import com.google.common.collect.Sets; 032 import com.google.common.collect.TreeMultiset; 033 034 import cpw.mods.fml.common.FMLLog; 035 import cpw.mods.fml.common.Loader; 036 import cpw.mods.fml.common.ModContainer; 037 038 import net.minecraft.src.Chunk; 039 import net.minecraft.src.ChunkCoordIntPair; 040 import net.minecraft.src.CompressedStreamTools; 041 import net.minecraft.src.Entity; 042 import net.minecraft.src.MathHelper; 043 import net.minecraft.src.NBTBase; 044 import net.minecraft.src.NBTTagCompound; 045 import net.minecraft.src.NBTTagList; 046 import net.minecraft.src.World; 047 import net.minecraft.src.WorldServer; 048 import net.minecraftforge.common.ForgeChunkManager.Ticket; 049 050 /** 051 * Manages chunkloading for mods. 052 * 053 * The basic principle is a ticket based system. 054 * 1. Mods register a callback {@link #setForcedChunkLoadingCallback(Object, LoadingCallback)} 055 * 2. Mods ask for a ticket {@link #requestTicket(Object, World, Type)} and then hold on to that ticket. 056 * 3. Mods request chunks to stay loaded {@link #forceChunk(Ticket, ChunkCoordIntPair)} or remove chunks from force loading {@link #unforceChunk(Ticket, ChunkCoordIntPair)}. 057 * 4. When a world unloads, the tickets associated with that world are saved by the chunk manager. 058 * 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 059 * chunks to stay loaded (and maybe take other actions). 060 * 061 * The chunkloading is configurable at runtime. The file "config/forgeChunkLoading.cfg" contains both default configuration for chunkloading, and a sample individual mod 062 * specific override section. 063 * 064 * @author cpw 065 * 066 */ 067 public class ForgeChunkManager 068 { 069 private static int defaultMaxCount; 070 private static int defaultMaxChunks; 071 private static boolean overridesEnabled; 072 073 private static Map<World, Multimap<String, Ticket>> tickets = Maps.newHashMap(); 074 private static Map<String, Integer> ticketConstraints = Maps.newHashMap(); 075 private static Map<String, Integer> chunkConstraints = Maps.newHashMap(); 076 077 private static Map<String, LoadingCallback> callbacks = Maps.newHashMap(); 078 079 private static Map<World, SetMultimap<ChunkCoordIntPair,Ticket>> forcedChunks = Maps.newHashMap(); 080 private static BiMap<UUID,Ticket> pendingEntities = HashBiMap.create(); 081 082 private static Cache<Long, Chunk> dormantChunkCache; 083 /** 084 * All mods requiring chunkloading need to implement this to handle the 085 * re-registration of chunk tickets at world loading time 086 * 087 * @author cpw 088 * 089 */ 090 public interface LoadingCallback 091 { 092 /** 093 * Called back when tickets are loaded from the world to allow the 094 * mod to re-register the chunks associated with those tickets. The list supplied 095 * here is truncated to length prior to use. Tickets unwanted by the 096 * mod must be disposed of manually unless the mod is an OrderedLoadingCallback instance 097 * in which case, they will have been disposed of by the earlier callback. 098 * 099 * @param tickets The tickets to re-register. The list is immutable and cannot be manipulated directly. Copy it first. 100 * @param world the world 101 */ 102 public void ticketsLoaded(List<Ticket> tickets, World world); 103 } 104 105 /** 106 * This is a special LoadingCallback that can be implemented as well as the 107 * LoadingCallback to provide access to additional behaviour. 108 * Specifically, this callback will fire prior to Forge dropping excess 109 * tickets. Tickets in the returned list are presumed ordered and excess will 110 * be truncated from the returned list. 111 * This allows the mod to control not only if they actually <em>want</em> a ticket but 112 * also their preferred ticket ordering. 113 * 114 * @author cpw 115 * 116 */ 117 public interface OrderedLoadingCallback extends LoadingCallback 118 { 119 /** 120 * Called back when tickets are loaded from the world to allow the 121 * mod to decide if it wants the ticket still, and prioritise overflow 122 * based on the ticket count. 123 * WARNING: You cannot force chunks in this callback, it is strictly for allowing the mod 124 * to be more selective in which tickets it wishes to preserve in an overflow situation 125 * 126 * @param tickets The tickets that you will want to select from. The list is immutable and cannot be manipulated directly. Copy it first. 127 * @param world The world 128 * @param maxTicketCount The maximum number of tickets that will be allowed. 129 * @return A list of the tickets this mod wishes to continue using. This list will be truncated 130 * to "maxTicketCount" size after the call returns and then offered to the other callback 131 * method 132 */ 133 public List<Ticket> ticketsLoaded(List<Ticket> tickets, World world, int maxTicketCount); 134 } 135 public enum Type 136 { 137 138 /** 139 * For non-entity registrations 140 */ 141 NORMAL, 142 /** 143 * For entity registrations 144 */ 145 ENTITY 146 } 147 public static class Ticket 148 { 149 private String modId; 150 private Type ticketType; 151 private LinkedHashSet<ChunkCoordIntPair> requestedChunks; 152 private NBTTagCompound modData; 153 private World world; 154 private int maxDepth; 155 private String entityClazz; 156 private int entityChunkX; 157 private int entityChunkZ; 158 private Entity entity; 159 160 Ticket(String modId, Type type, World world) 161 { 162 this.modId = modId; 163 this.ticketType = type; 164 this.world = world; 165 this.maxDepth = getMaxChunkDepthFor(modId); 166 this.requestedChunks = Sets.newLinkedHashSet(); 167 } 168 169 /** 170 * 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, 171 * the least recently forced chunk, by original registration time, is removed from the forced chunk list. 172 * 173 * @param depth The new depth to set 174 */ 175 public void setChunkListDepth(int depth) 176 { 177 if (depth > getMaxChunkDepthFor(modId) || (depth <= 0 && getMaxChunkDepthFor(modId) > 0)) 178 { 179 FMLLog.warning("The mod %s tried to modify the chunk ticket depth to: %d, its allowed maximum is: %d", modId, depth, getMaxChunkDepthFor(modId)); 180 } 181 else 182 { 183 this.maxDepth = depth; 184 } 185 } 186 /** 187 * Get the maximum chunk depth size 188 * 189 * @return The maximum chunk depth size 190 */ 191 public int getMaxChunkListDepth() 192 { 193 return getMaxChunkDepthFor(modId); 194 } 195 196 /** 197 * Bind the entity to the ticket for {@link Type#ENTITY} type tickets. Other types will throw a runtime exception. 198 * 199 * @param entity The entity to bind 200 */ 201 public void bindEntity(Entity entity) 202 { 203 if (ticketType!=Type.ENTITY) 204 { 205 throw new RuntimeException("Cannot bind an entity to a non-entity ticket"); 206 } 207 this.entity = entity; 208 } 209 210 /** 211 * Retrieve the {@link NBTTagCompound} that stores mod specific data for the chunk ticket. 212 * Example data to store would be a TileEntity or Block location. This is persisted with the ticket and 213 * provided to the {@link LoadingCallback} for the mod. It is recommended to use this to recover 214 * useful state information for the forced chunks. 215 * 216 * @return The custom compound tag for mods to store additional chunkloading data 217 */ 218 public NBTTagCompound getModData() 219 { 220 if (this.modData == null) 221 { 222 this.modData = new NBTTagCompound(); 223 } 224 return modData; 225 } 226 227 /** 228 * Get the entity associated with this {@link Type#ENTITY} type ticket 229 * @return 230 */ 231 public Entity getEntity() 232 { 233 return entity; 234 } 235 } 236 237 static void loadWorld(World world) 238 { 239 ArrayListMultimap<String, Ticket> newTickets = ArrayListMultimap.<String, Ticket>create(); 240 tickets.put(world, newTickets); 241 242 SetMultimap<ChunkCoordIntPair,Ticket> forcedChunkMap = LinkedHashMultimap.create(); 243 forcedChunks.put(world, forcedChunkMap); 244 245 if (!(world instanceof WorldServer)) 246 { 247 return; 248 } 249 250 WorldServer worldServer = (WorldServer) world; 251 File chunkDir = worldServer.getChunkSaveLocation(); 252 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat"); 253 254 if (chunkLoaderData.exists() && chunkLoaderData.isFile()) 255 { 256 ArrayListMultimap<String, Ticket> loadedTickets = ArrayListMultimap.<String, Ticket>create(); 257 NBTTagCompound forcedChunkData; 258 try 259 { 260 forcedChunkData = CompressedStreamTools.read(chunkLoaderData); 261 } 262 catch (IOException e) 263 { 264 FMLLog.log(Level.WARNING, e, "Unable to read forced chunk data at %s - it will be ignored", chunkLoaderData.getAbsolutePath()); 265 return; 266 } 267 NBTTagList ticketList = forcedChunkData.getTagList("TicketList"); 268 for (int i = 0; i < ticketList.tagCount(); i++) 269 { 270 NBTTagCompound ticketHolder = (NBTTagCompound) ticketList.tagAt(i); 271 String modId = ticketHolder.getString("Owner"); 272 273 if (!Loader.isModLoaded(modId)) 274 { 275 FMLLog.warning("Found chunkloading data for mod %s which is currently not available or active - it will be removed from the world save", modId); 276 continue; 277 } 278 279 if (!callbacks.containsKey(modId)) 280 { 281 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); 282 continue; 283 } 284 285 NBTTagList tickets = ticketHolder.getTagList("Tickets"); 286 for (int j = 0; j < tickets.tagCount(); j++) 287 { 288 NBTTagCompound ticket = (NBTTagCompound) tickets.tagAt(j); 289 Type type = Type.values()[ticket.getByte("Type")]; 290 byte ticketChunkDepth = ticket.getByte("ChunkListDepth"); 291 Ticket tick = new Ticket(modId, type, world); 292 if (ticket.hasKey("ModData")) 293 { 294 tick.modData = ticket.getCompoundTag("ModData"); 295 } 296 if (type == Type.ENTITY) 297 { 298 tick.entityChunkX = ticket.getInteger("chunkX"); 299 tick.entityChunkZ = ticket.getInteger("chunkZ"); 300 UUID uuid = new UUID(ticket.getLong("PersistentIDMSB"), ticket.getLong("PersistentIDLSB")); 301 // add the ticket to the "pending entity" list 302 pendingEntities.put(uuid, tick); 303 } 304 loadedTickets.put(modId, tick); 305 } 306 } 307 308 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values())) 309 { 310 if (tick.ticketType == Type.ENTITY && tick.entity == null) 311 { 312 // force the world to load the entity's chunk 313 // the load will come back through the loadEntity method and attach the entity 314 // to the ticket 315 world.getChunkFromChunkCoords(tick.entityChunkX, tick.entityChunkZ); 316 } 317 } 318 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values())) 319 { 320 if (tick.ticketType == Type.ENTITY && tick.entity == null) 321 { 322 FMLLog.warning("Failed to load persistent chunkloading entity %s from store.", pendingEntities.inverse().get(tick)); 323 loadedTickets.remove(tick.modId, tick); 324 } 325 } 326 pendingEntities.clear(); 327 // send callbacks 328 for (String modId : loadedTickets.keySet()) 329 { 330 LoadingCallback loadingCallback = callbacks.get(modId); 331 int maxTicketLength = getMaxTicketLengthFor(modId); 332 List<Ticket> tickets = loadedTickets.get(modId); 333 if (loadingCallback instanceof OrderedLoadingCallback) 334 { 335 OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback; 336 tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength); 337 } 338 if (tickets.size() > maxTicketLength) 339 { 340 FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size()); 341 tickets.subList(maxTicketLength, tickets.size()).clear(); 342 } 343 ForgeChunkManager.tickets.get(world).putAll(modId, tickets); 344 loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world); 345 } 346 } 347 } 348 349 /** 350 * Set a chunkloading callback for the supplied mod object 351 * 352 * @param mod The mod instance registering the callback 353 * @param callback The code to call back when forced chunks are loaded 354 */ 355 public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback) 356 { 357 ModContainer container = getContainer(mod); 358 if (container == null) 359 { 360 FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 361 return; 362 } 363 364 callbacks.put(container.getModId(), callback); 365 } 366 367 /** 368 * Discover the available tickets for the mod in the world 369 * 370 * @param mod The mod that will own the tickets 371 * @param world The world 372 * @return The count of tickets left for the mod in the supplied world 373 */ 374 public static int ticketCountAvailableFor(Object mod, World world) 375 { 376 ModContainer container = getContainer(mod); 377 if (container!=null) 378 { 379 String modId = container.getModId(); 380 int allowedCount = getMaxTicketLengthFor(modId); 381 return allowedCount - tickets.get(world).get(modId).size(); 382 } 383 else 384 { 385 return 0; 386 } 387 } 388 389 private static ModContainer getContainer(Object mod) 390 { 391 ModContainer container = Loader.instance().getModObjectList().inverse().get(mod); 392 return container; 393 } 394 395 private static int getMaxTicketLengthFor(String modId) 396 { 397 int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount; 398 return allowedCount; 399 } 400 401 private static int getMaxChunkDepthFor(String modId) 402 { 403 int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks; 404 return allowedCount; 405 } 406 /** 407 * Request a chunkloading ticket of the appropriate type for the supplied mod 408 * 409 * @param mod The mod requesting a ticket 410 * @param world The world in which it is requesting the ticket 411 * @param type The type of ticket 412 * @return A ticket with which to register chunks for loading, or null if no further tickets are available 413 */ 414 public static Ticket requestTicket(Object mod, World world, Type type) 415 { 416 ModContainer container = getContainer(mod); 417 if (container == null) 418 { 419 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 420 return null; 421 } 422 String modId = container.getModId(); 423 if (!callbacks.containsKey(modId)) 424 { 425 FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId); 426 throw new RuntimeException("Invalid ticket request"); 427 } 428 429 int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount; 430 431 if (tickets.get(world).get(modId).size() >= allowedCount) 432 { 433 FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount); 434 return null; 435 } 436 Ticket ticket = new Ticket(modId, type, world); 437 tickets.get(world).put(modId, ticket); 438 439 return ticket; 440 } 441 442 /** 443 * 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. 444 * 445 * @param ticket The ticket to release 446 */ 447 public static void releaseTicket(Ticket ticket) 448 { 449 if (ticket == null) 450 { 451 return; 452 } 453 if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 454 { 455 return; 456 } 457 if (ticket.requestedChunks!=null) 458 { 459 for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks)) 460 { 461 unforceChunk(ticket, chunk); 462 } 463 } 464 tickets.get(ticket.world).remove(ticket.modId, ticket); 465 } 466 467 /** 468 * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least 469 * recently registered chunk is unforced and may be unloaded. 470 * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering. 471 * 472 * @param ticket The ticket registering the chunk 473 * @param chunk The chunk to force 474 */ 475 public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk) 476 { 477 if (ticket == null || chunk == null) 478 { 479 return; 480 } 481 if (ticket.ticketType == Type.ENTITY && ticket.entity == null) 482 { 483 throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity"); 484 } 485 if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 486 { 487 FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId); 488 return; 489 } 490 ticket.requestedChunks.add(chunk); 491 forcedChunks.get(ticket.world).put(chunk, ticket); 492 if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth) 493 { 494 ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next(); 495 unforceChunk(ticket,removed); 496 } 497 } 498 499 /** 500 * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list 501 * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks 502 * in the ticket list 503 * 504 * @param ticket The ticket holding the chunk list 505 * @param chunk The chunk you wish to push to the end (so that it would be unloaded last) 506 */ 507 public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk) 508 { 509 if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk)) 510 { 511 return; 512 } 513 ticket.requestedChunks.remove(chunk); 514 ticket.requestedChunks.add(chunk); 515 } 516 /** 517 * Unforce the supplied chunk, allowing it to be unloaded and stop ticking. 518 * 519 * @param ticket The ticket holding the chunk 520 * @param chunk The chunk to unforce 521 */ 522 public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk) 523 { 524 if (ticket == null || chunk == null) 525 { 526 return; 527 } 528 ticket.requestedChunks.remove(chunk); 529 forcedChunks.get(ticket.world).remove(chunk, ticket); 530 } 531 532 static void loadConfiguration(File configDir) 533 { 534 File cfgFile = new File(configDir,"forgeChunkLoading.cfg"); 535 Configuration config = new Configuration(cfgFile, true); 536 try 537 { 538 config.categories.clear(); 539 try 540 { 541 config.load(); 542 } 543 catch (Exception e) 544 { 545 File dest = new File(configDir,"forgeChunkLoading.cfg.bak"); 546 if (dest.exists()) 547 { 548 dest.delete(); 549 } 550 cfgFile.renameTo(dest); 551 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"); 552 } 553 config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control"); 554 Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200); 555 maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" + 556 "in this file. This is the number of chunk loading requests a mod is allowed to make."; 557 defaultMaxCount = maxTicketCount.getInt(200); 558 559 Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25); 560 maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" + 561 "for a mod without an override. This is the maximum number of chunks a single ticket can force."; 562 defaultMaxChunks = maxChunks.getInt(25); 563 564 Property dormantChunkCacheSize = config.get("defaults", "dormantChunkCacheSize", 0); 565 dormantChunkCacheSize.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" + 566 "loading times. Specify the size of that cache here"; 567 dormantChunkCache = CacheBuilder.newBuilder().maximumSize(dormantChunkCacheSize.getInt(0)).build(); 568 FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSize.getInt(0)); 569 570 Property modOverridesEnabled = config.get("defaults", "enabled", true); 571 modOverridesEnabled.comment = "Are mod overrides enabled?"; 572 overridesEnabled = modOverridesEnabled.getBoolean(true); 573 574 config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" + 575 "Copy this section and rename the with the modid for the mod you wish to override.\n" + 576 "A value of zero in either entry effectively disables any chunkloading capabilities\n" + 577 "for that mod"); 578 579 Property sampleTC = config.get("Forge", "maximumTicketCount", 200); 580 sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities."; 581 sampleTC = config.get("Forge", "maximumChunksPerTicket", 25); 582 sampleTC.comment = "Maximum chunks per ticket for the mod."; 583 for (String mod : config.categories.keySet()) 584 { 585 if (mod.equals("Forge") || mod.equals("defaults")) 586 { 587 continue; 588 } 589 Property modTC = config.get(mod, "maximumTicketCount", 200); 590 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25); 591 ticketConstraints.put(mod, modTC.getInt(200)); 592 chunkConstraints.put(mod, modCPT.getInt(25)); 593 } 594 } 595 finally 596 { 597 config.save(); 598 if (dormantChunkCache == null) 599 { 600 dormantChunkCache = CacheBuilder.newBuilder().maximumSize(0).build(); 601 FMLLog.info("Configured a dormant chunk cache size of 0"); 602 } 603 } 604 605 } 606 607 /** 608 * The list of persistent chunks in the world. This set is immutable. 609 * @param world 610 * @return 611 */ 612 public static SetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world) 613 { 614 return forcedChunks.containsKey(world) ? ImmutableSetMultimap.copyOf(forcedChunks.get(world)) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of(); 615 } 616 617 static void saveWorld(World world) 618 { 619 // only persist persistent worlds 620 if (!(world instanceof WorldServer)) { return; } 621 WorldServer worldServer = (WorldServer) world; 622 File chunkDir = worldServer.getChunkSaveLocation(); 623 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat"); 624 625 NBTTagCompound forcedChunkData = new NBTTagCompound(); 626 NBTTagList ticketList = new NBTTagList(); 627 forcedChunkData.setTag("TicketList", ticketList); 628 629 Multimap<String, Ticket> ticketSet = tickets.get(worldServer); 630 for (String modId : ticketSet.keySet()) 631 { 632 NBTTagCompound ticketHolder = new NBTTagCompound(); 633 ticketList.appendTag(ticketHolder); 634 635 ticketHolder.setString("Owner", modId); 636 NBTTagList tickets = new NBTTagList(); 637 ticketHolder.setTag("Tickets", tickets); 638 639 for (Ticket tick : ticketSet.get(modId)) 640 { 641 NBTTagCompound ticket = new NBTTagCompound(); 642 ticket.setByte("Type", (byte) tick.ticketType.ordinal()); 643 ticket.setByte("ChunkListDepth", (byte) tick.maxDepth); 644 if (tick.modData != null) 645 { 646 ticket.setCompoundTag("ModData", tick.modData); 647 } 648 if (tick.ticketType == Type.ENTITY && tick.entity != null) 649 { 650 ticket.setInteger("chunkX", MathHelper.floor_double(tick.entity.chunkCoordX)); 651 ticket.setInteger("chunkZ", MathHelper.floor_double(tick.entity.chunkCoordZ)); 652 ticket.setLong("PersistentIDMSB", tick.entity.getPersistentID().getMostSignificantBits()); 653 ticket.setLong("PersistentIDLSB", tick.entity.getPersistentID().getLeastSignificantBits()); 654 tickets.appendTag(ticket); 655 } 656 else if (tick.ticketType != Type.ENTITY) 657 { 658 tickets.appendTag(ticket); 659 } 660 } 661 } 662 try 663 { 664 CompressedStreamTools.write(forcedChunkData, chunkLoaderData); 665 } 666 catch (IOException e) 667 { 668 FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath()); 669 return; 670 } 671 } 672 673 static void loadEntity(Entity entity) 674 { 675 UUID id = entity.getPersistentID(); 676 Ticket tick = pendingEntities.get(id); 677 if (tick != null) 678 { 679 tick.bindEntity(entity); 680 pendingEntities.remove(id); 681 } 682 } 683 684 public static void putDormantChunk(long coords, Chunk chunk) 685 { 686 dormantChunkCache.put(coords, chunk); 687 } 688 689 public static Chunk fetchDormantChunk(long coords) 690 { 691 return dormantChunkCache.getIfPresent(coords); 692 } 693 }