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