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> loadedTickets = ArrayListMultimap.<String, Ticket>create(); 240 tickets.put(world, loadedTickets); 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 NBTTagCompound forcedChunkData; 257 try 258 { 259 forcedChunkData = CompressedStreamTools.read(chunkLoaderData); 260 } 261 catch (IOException e) 262 { 263 FMLLog.log(Level.WARNING, e, "Unable to read forced chunk data at %s - it will be ignored", chunkLoaderData.getAbsolutePath()); 264 return; 265 } 266 NBTTagList ticketList = forcedChunkData.getTagList("TicketList"); 267 for (int i = 0; i < ticketList.tagCount(); i++) 268 { 269 NBTTagCompound ticketHolder = (NBTTagCompound) ticketList.tagAt(i); 270 String modId = ticketHolder.getString("Owner"); 271 272 if (!Loader.isModLoaded(modId)) 273 { 274 FMLLog.warning("Found chunkloading data for mod %s which is currently not available or active - it will be removed from the world save", modId); 275 continue; 276 } 277 278 if (!callbacks.containsKey(modId)) 279 { 280 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); 281 continue; 282 } 283 284 NBTTagList tickets = ticketHolder.getTagList("Tickets"); 285 for (int j = 0; j < tickets.tagCount(); j++) 286 { 287 NBTTagCompound ticket = (NBTTagCompound) tickets.tagAt(j); 288 Type type = Type.values()[ticket.getByte("Type")]; 289 byte ticketChunkDepth = ticket.getByte("ChunkListDepth"); 290 Ticket tick = new Ticket(modId, type, world); 291 if (ticket.hasKey("ModData")) 292 { 293 tick.modData = ticket.getCompoundTag("ModData"); 294 } 295 if (type == Type.ENTITY) 296 { 297 tick.entityChunkX = ticket.getInteger("chunkX"); 298 tick.entityChunkZ = ticket.getInteger("chunkZ"); 299 UUID uuid = new UUID(ticket.getLong("PersistentIDMSB"), ticket.getLong("PersistentIDLSB")); 300 // add the ticket to the "pending entity" list 301 pendingEntities.put(uuid, tick); 302 } 303 loadedTickets.put(modId, tick); 304 } 305 } 306 307 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values())) 308 { 309 if (tick.ticketType == Type.ENTITY && tick.entity == null) 310 { 311 // force the world to load the entity's chunk 312 // the load will come back through the loadEntity method and attach the entity 313 // to the ticket 314 world.getChunkFromChunkCoords(tick.entityChunkX, tick.entityChunkZ); 315 } 316 } 317 for (Ticket tick : ImmutableSet.copyOf(pendingEntities.values())) 318 { 319 if (tick.ticketType == Type.ENTITY && tick.entity == null) 320 { 321 FMLLog.warning("Failed to load persistent chunkloading entity %s from store.", pendingEntities.inverse().get(tick)); 322 loadedTickets.remove(tick.modId, tick); 323 } 324 } 325 pendingEntities.clear(); 326 // send callbacks 327 for (String modId : loadedTickets.keySet()) 328 { 329 LoadingCallback loadingCallback = callbacks.get(modId); 330 int maxTicketLength = getMaxTicketLengthFor(modId); 331 List<Ticket> tickets = loadedTickets.get(modId); 332 if (loadingCallback instanceof OrderedLoadingCallback) 333 { 334 OrderedLoadingCallback orderedLoadingCallback = (OrderedLoadingCallback) loadingCallback; 335 tickets = orderedLoadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world, maxTicketLength); 336 } 337 if (tickets.size() > maxTicketLength) 338 { 339 FMLLog.warning("The mod %s has too many open chunkloading tickets %d. Excess will be dropped", modId, tickets.size()); 340 tickets.subList(maxTicketLength, tickets.size()).clear(); 341 } 342 ForgeChunkManager.tickets.get(world).putAll(modId, tickets); 343 loadingCallback.ticketsLoaded(ImmutableList.copyOf(tickets), world); 344 } 345 } 346 } 347 348 /** 349 * Set a chunkloading callback for the supplied mod object 350 * 351 * @param mod The mod instance registering the callback 352 * @param callback The code to call back when forced chunks are loaded 353 */ 354 public static void setForcedChunkLoadingCallback(Object mod, LoadingCallback callback) 355 { 356 ModContainer container = getContainer(mod); 357 if (container == null) 358 { 359 FMLLog.warning("Unable to register a callback for an unknown mod %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 360 return; 361 } 362 363 callbacks.put(container.getModId(), callback); 364 } 365 366 /** 367 * Discover the available tickets for the mod in the world 368 * 369 * @param mod The mod that will own the tickets 370 * @param world The world 371 * @return The count of tickets left for the mod in the supplied world 372 */ 373 public static int ticketCountAvailableFor(Object mod, World world) 374 { 375 ModContainer container = getContainer(mod); 376 if (container!=null) 377 { 378 String modId = container.getModId(); 379 int allowedCount = getMaxTicketLengthFor(modId); 380 return allowedCount - tickets.get(world).get(modId).size(); 381 } 382 else 383 { 384 return 0; 385 } 386 } 387 388 private static ModContainer getContainer(Object mod) 389 { 390 ModContainer container = Loader.instance().getModObjectList().inverse().get(mod); 391 return container; 392 } 393 394 private static int getMaxTicketLengthFor(String modId) 395 { 396 int allowedCount = ticketConstraints.containsKey(modId) && overridesEnabled ? ticketConstraints.get(modId) : defaultMaxCount; 397 return allowedCount; 398 } 399 400 private static int getMaxChunkDepthFor(String modId) 401 { 402 int allowedCount = chunkConstraints.containsKey(modId) && overridesEnabled ? chunkConstraints.get(modId) : defaultMaxChunks; 403 return allowedCount; 404 } 405 /** 406 * Request a chunkloading ticket of the appropriate type for the supplied mod 407 * 408 * @param mod The mod requesting a ticket 409 * @param world The world in which it is requesting the ticket 410 * @param type The type of ticket 411 * @return A ticket with which to register chunks for loading, or null if no further tickets are available 412 */ 413 public static Ticket requestTicket(Object mod, World world, Type type) 414 { 415 ModContainer container = getContainer(mod); 416 if (container == null) 417 { 418 FMLLog.log(Level.SEVERE, "Failed to locate the container for mod instance %s (%s : %x)", mod, mod.getClass().getName(), System.identityHashCode(mod)); 419 return null; 420 } 421 String modId = container.getModId(); 422 if (!callbacks.containsKey(modId)) 423 { 424 FMLLog.severe("The mod %s has attempted to request a ticket without a listener in place", modId); 425 throw new RuntimeException("Invalid ticket request"); 426 } 427 428 int allowedCount = ticketConstraints.containsKey(modId) ? ticketConstraints.get(modId) : defaultMaxCount; 429 430 if (tickets.get(world).get(modId).size() >= allowedCount) 431 { 432 FMLLog.info("The mod %s has attempted to allocate a chunkloading ticket beyond it's currently allocated maximum : %d", modId, allowedCount); 433 return null; 434 } 435 Ticket ticket = new Ticket(modId, type, world); 436 tickets.get(world).put(modId, ticket); 437 438 return ticket; 439 } 440 441 /** 442 * 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. 443 * 444 * @param ticket The ticket to release 445 */ 446 public static void releaseTicket(Ticket ticket) 447 { 448 if (ticket == null) 449 { 450 return; 451 } 452 if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 453 { 454 return; 455 } 456 if (ticket.requestedChunks!=null) 457 { 458 for (ChunkCoordIntPair chunk : ImmutableSet.copyOf(ticket.requestedChunks)) 459 { 460 unforceChunk(ticket, chunk); 461 } 462 } 463 tickets.get(ticket.world).remove(ticket.modId, ticket); 464 } 465 466 /** 467 * Force the supplied chunk coordinate to be loaded by the supplied ticket. If the ticket's {@link Ticket#maxDepth} is exceeded, the least 468 * recently registered chunk is unforced and may be unloaded. 469 * It is safe to force the chunk several times for a ticket, it will not generate duplication or change the ordering. 470 * 471 * @param ticket The ticket registering the chunk 472 * @param chunk The chunk to force 473 */ 474 public static void forceChunk(Ticket ticket, ChunkCoordIntPair chunk) 475 { 476 if (ticket == null || chunk == null) 477 { 478 return; 479 } 480 if (ticket.ticketType == Type.ENTITY && ticket.entity == null) 481 { 482 throw new RuntimeException("Attempted to use an entity ticket to force a chunk, without an entity"); 483 } 484 if (!tickets.get(ticket.world).containsEntry(ticket.modId, ticket)) 485 { 486 FMLLog.severe("The mod %s attempted to force load a chunk with an invalid ticket. This is not permitted.", ticket.modId); 487 return; 488 } 489 ticket.requestedChunks.add(chunk); 490 forcedChunks.get(ticket.world).put(chunk, ticket); 491 if (ticket.maxDepth > 0 && ticket.requestedChunks.size() > ticket.maxDepth) 492 { 493 ChunkCoordIntPair removed = ticket.requestedChunks.iterator().next(); 494 unforceChunk(ticket,removed); 495 } 496 } 497 498 /** 499 * Reorganize the internal chunk list so that the chunk supplied is at the *end* of the list 500 * This helps if you wish to guarantee a certain "automatic unload ordering" for the chunks 501 * in the ticket list 502 * 503 * @param ticket The ticket holding the chunk list 504 * @param chunk The chunk you wish to push to the end (so that it would be unloaded last) 505 */ 506 public static void reorderChunk(Ticket ticket, ChunkCoordIntPair chunk) 507 { 508 if (ticket == null || chunk == null || !ticket.requestedChunks.contains(chunk)) 509 { 510 return; 511 } 512 ticket.requestedChunks.remove(chunk); 513 ticket.requestedChunks.add(chunk); 514 } 515 /** 516 * Unforce the supplied chunk, allowing it to be unloaded and stop ticking. 517 * 518 * @param ticket The ticket holding the chunk 519 * @param chunk The chunk to unforce 520 */ 521 public static void unforceChunk(Ticket ticket, ChunkCoordIntPair chunk) 522 { 523 if (ticket == null || chunk == null) 524 { 525 return; 526 } 527 ticket.requestedChunks.remove(chunk); 528 forcedChunks.get(ticket.world).remove(chunk, ticket); 529 } 530 531 static void loadConfiguration(File configDir) 532 { 533 File cfgFile = new File(configDir,"forgeChunkLoading.cfg"); 534 Configuration config = new Configuration(cfgFile, true); 535 try 536 { 537 config.categories.clear(); 538 try 539 { 540 config.load(); 541 } 542 catch (Exception e) 543 { 544 File dest = new File(configDir,"forgeChunkLoading.cfg.bak"); 545 if (dest.exists()) 546 { 547 dest.delete(); 548 } 549 cfgFile.renameTo(dest); 550 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"); 551 } 552 config.addCustomCategoryComment("defaults", "Default configuration for forge chunk loading control"); 553 Property maxTicketCount = config.get("defaults", "maximumTicketCount", 200); 554 maxTicketCount.comment = "The default maximum ticket count for a mod which does not have an override\n" + 555 "in this file. This is the number of chunk loading requests a mod is allowed to make."; 556 defaultMaxCount = maxTicketCount.getInt(200); 557 558 Property maxChunks = config.get("defaults", "maximumChunksPerTicket", 25); 559 maxChunks.comment = "The default maximum number of chunks a mod can force, per ticket, \n" + 560 "for a mod without an override. This is the maximum number of chunks a single ticket can force."; 561 defaultMaxChunks = maxChunks.getInt(25); 562 563 Property dormantChunkCacheSize = config.get("defaults", "dormantChunkCacheSize", 0); 564 dormantChunkCacheSize.comment = "Unloaded chunks can first be kept in a dormant cache for quicker\n" + 565 "loading times. Specify the size of that cache here"; 566 dormantChunkCache = CacheBuilder.newBuilder().maximumSize(dormantChunkCacheSize.getInt(0)).build(); 567 FMLLog.info("Configured a dormant chunk cache size of %d", dormantChunkCacheSize.getInt(0)); 568 569 Property modOverridesEnabled = config.get("defaults", "enabled", true); 570 modOverridesEnabled.comment = "Are mod overrides enabled?"; 571 overridesEnabled = modOverridesEnabled.getBoolean(true); 572 573 config.addCustomCategoryComment("Forge", "Sample mod specific control section.\n" + 574 "Copy this section and rename the with the modid for the mod you wish to override.\n" + 575 "A value of zero in either entry effectively disables any chunkloading capabilities\n" + 576 "for that mod"); 577 578 Property sampleTC = config.get("Forge", "maximumTicketCount", 200); 579 sampleTC.comment = "Maximum ticket count for the mod. Zero disables chunkloading capabilities."; 580 sampleTC = config.get("Forge", "maximumChunksPerTicket", 25); 581 sampleTC.comment = "Maximum chunks per ticket for the mod."; 582 for (String mod : config.categories.keySet()) 583 { 584 if (mod.equals("Forge") || mod.equals("defaults")) 585 { 586 continue; 587 } 588 Property modTC = config.get(mod, "maximumTicketCount", 200); 589 Property modCPT = config.get(mod, "maximumChunksPerTicket", 25); 590 ticketConstraints.put(mod, modTC.getInt(200)); 591 chunkConstraints.put(mod, modCPT.getInt(25)); 592 } 593 } 594 finally 595 { 596 config.save(); 597 if (dormantChunkCache == null) 598 { 599 dormantChunkCache = CacheBuilder.newBuilder().maximumSize(0).build(); 600 FMLLog.info("Configured a dormant chunk cache size of 0"); 601 } 602 } 603 604 } 605 606 /** 607 * The list of persistent chunks in the world. This set is immutable. 608 * @param world 609 * @return 610 */ 611 public static SetMultimap<ChunkCoordIntPair, Ticket> getPersistentChunksFor(World world) 612 { 613 return forcedChunks.containsKey(world) ? ImmutableSetMultimap.copyOf(forcedChunks.get(world)) : ImmutableSetMultimap.<ChunkCoordIntPair,Ticket>of(); 614 } 615 616 static void saveWorld(World world) 617 { 618 // only persist persistent worlds 619 if (!(world instanceof WorldServer)) { return; } 620 WorldServer worldServer = (WorldServer) world; 621 File chunkDir = worldServer.getChunkSaveLocation(); 622 File chunkLoaderData = new File(chunkDir, "forcedchunks.dat"); 623 624 NBTTagCompound forcedChunkData = new NBTTagCompound(); 625 NBTTagList ticketList = new NBTTagList(); 626 forcedChunkData.setTag("TicketList", ticketList); 627 628 Multimap<String, Ticket> ticketSet = tickets.get(worldServer); 629 for (String modId : ticketSet.keySet()) 630 { 631 NBTTagCompound ticketHolder = new NBTTagCompound(); 632 ticketList.appendTag(ticketHolder); 633 634 ticketHolder.setString("Owner", modId); 635 NBTTagList tickets = new NBTTagList(); 636 ticketHolder.setTag("Tickets", tickets); 637 638 for (Ticket tick : ticketSet.get(modId)) 639 { 640 NBTTagCompound ticket = new NBTTagCompound(); 641 tickets.appendTag(ticket); 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) 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 } 655 } 656 } 657 try 658 { 659 CompressedStreamTools.write(forcedChunkData, chunkLoaderData); 660 } 661 catch (IOException e) 662 { 663 FMLLog.log(Level.WARNING, e, "Unable to write forced chunk data to %s - chunkloading won't work", chunkLoaderData.getAbsolutePath()); 664 return; 665 } 666 } 667 668 static void loadEntity(Entity entity) 669 { 670 UUID id = entity.getPersistentID(); 671 Ticket tick = pendingEntities.get(id); 672 if (tick != null) 673 { 674 tick.bindEntity(entity); 675 pendingEntities.remove(id); 676 } 677 } 678 679 public static void putDormantChunk(long coords, Chunk chunk) 680 { 681 dormantChunkCache.put(coords, chunk); 682 } 683 684 public static Chunk fetchDormantChunk(long coords) 685 { 686 return dormantChunkCache.getIfPresent(coords); 687 } 688 }