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