diff --git a/Spigot-API-Patches/0242-NamespacedKey-based-plugin-chunk-tickets.patch b/Spigot-API-Patches/0242-NamespacedKey-based-plugin-chunk-tickets.patch new file mode 100644 index 000000000000..ffbb2af9a419 --- /dev/null +++ b/Spigot-API-Patches/0242-NamespacedKey-based-plugin-chunk-tickets.patch @@ -0,0 +1,328 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Trigary +Date: Sat, 22 Aug 2020 14:41:43 +0200 +Subject: [PATCH] NamespacedKey based plugin chunk tickets + + +diff --git a/src/main/java/io/papermc/paper/world/TickingLevel.java b/src/main/java/io/papermc/paper/world/TickingLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64b77f2c7ca920a44d6628b3edacbaeb3f727b1f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/world/TickingLevel.java +@@ -0,0 +1,55 @@ ++package io.papermc.paper.world; ++ ++import org.apache.commons.lang.Validate; ++import org.bukkit.Chunk; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * Represents what kind of ticking is done on the {@link Chunk}. ++ */ ++public enum TickingLevel { ++ ++ /** ++ * Both blocks and entities get ticked in this level. ++ */ ++ ENTITY, ++ ++ /** ++ * Blocks get ticked in this level, but entities do not. ++ */ ++ BLOCK, ++ ++ /** ++ * Nothing gets ticked in this level, but the {@link Chunk} stays loaded. ++ */ ++ NONE; ++ ++ /** ++ * Gets whether this ticking level does at least as much as the specified one. ++ * Examples (all of these expressions evaluate to {@code true}): ++ * ++ * ++ * @param other the ticking level to test ++ * @return {@code true} if the current instance does at least everything ++ * the other one does, {@code false} otherwise ++ */ ++ @Contract(pure = true) ++ public boolean includes(@NotNull TickingLevel other) { ++ Validate.notNull(other, "Ticking level cannot be null"); ++ switch (this) { ++ case ENTITY: ++ return true; ++ case BLOCK: ++ return other != ENTITY; ++ case NONE: ++ return other == NONE; ++ default: ++ throw new AssertionError("Unhandled case: " + this); ++ } ++ } ++} +diff --git a/src/main/java/org/bukkit/Chunk.java b/src/main/java/org/bukkit/Chunk.java +index b4ef6297f78d1f0c216e718024a21e6aa07cd1c6..94c36228346a1b79529e2308f4654c80db4d6614 100644 +--- a/src/main/java/org/bukkit/Chunk.java ++++ b/src/main/java/org/bukkit/Chunk.java +@@ -8,6 +8,7 @@ import org.bukkit.entity.Entity; + import org.bukkit.persistence.PersistentDataHolder; + import org.bukkit.plugin.Plugin; + import org.jetbrains.annotations.NotNull; ++import io.papermc.paper.world.TickingLevel; // Paper + + /** + * Represents a chunk of blocks +@@ -240,6 +241,78 @@ public interface Chunk extends PersistentDataHolder { + @NotNull + Collection getPluginChunkTickets(); + ++ // Paper start ++ /** ++ * Adds a {@link NamespacedKey} based plugin chunk ticket ++ * with {@link TickingLevel#ENTITY}. ++ * If a ticket with a different level is already present, it will be removed. ++ * ++ * @param key the identifier of the ticket ++ * @return the {@link TickingLevel} that was overwritten ++ * or {@code null} if no ticket was present with the specified key ++ * @see World#addKeyedPluginChunkTicket(int, int, NamespacedKey) ++ */ ++ @org.jetbrains.annotations.Nullable ++ default TickingLevel addKeyedPluginChunkTicket(@NotNull NamespacedKey key) { ++ return getWorld().addKeyedPluginChunkTicket(getX(), getZ(), key); ++ } ++ ++ /** ++ * Adds a {@link NamespacedKey} based plugin chunk ticket ++ * with the specified {@link TickingLevel}. ++ * If a ticket with a different level is already present, it will be removed. ++ * ++ * @param key the identifier of the ticket ++ * @param level the level of the ticket ++ * @return the {@link TickingLevel} that was overwritten ++ * or {@code null} if no ticket was present with the specified key ++ * @see World#addKeyedPluginChunkTicket(int, int, NamespacedKey, TickingLevel) ++ */ ++ @org.jetbrains.annotations.Nullable ++ default TickingLevel addKeyedPluginChunkTicket(@NotNull NamespacedKey key, @NotNull TickingLevel level) { ++ return getWorld().addKeyedPluginChunkTicket(getX(), getZ(), key, level); ++ } ++ ++ /** ++ * Removes the {@link NamespacedKey} based plugin chunk ticket ++ * which has the specified key, if there is one. ++ * ++ * @param key the identifier of the tickets ++ * @return the {@link TickingLevel} that was removed ++ * or {@code null} if no ticket was present with the specified key ++ * @see World#removeKeyedPluginChunkTicket(int, int, NamespacedKey) ++ */ ++ @org.jetbrains.annotations.Nullable ++ default TickingLevel removeKeyedPluginChunkTicket(@NotNull NamespacedKey key) { ++ return getWorld().removeKeyedPluginChunkTicket(getX(), getZ(), key); ++ } ++ ++ /** ++ * Gets the {@link NamespacedKey} based plugin chunk ticket level ++ * with the specified key, if there is one. ++ * ++ * @param key the identifier of the tickets ++ * @return the level which is present with the specified key or {@code null} if there is none ++ * @see #getKeyedPluginChunkTicketLevel(NamespacedKey) ++ */ ++ @org.jetbrains.annotations.Nullable ++ default TickingLevel getKeyedPluginChunkTicketLevel(@NotNull NamespacedKey key) { ++ return getWorld().getKeyedPluginChunkTicketLevel(getX(), getZ(), key); ++ } ++ ++ /** ++ * Gets the keys of the {@link NamespacedKey} based plugin chunk tickets ++ * which are present in this chunk. ++ * ++ * @return the keys which are present ++ * @see World#getKeyedPluginChunkTicketKeys(int, int) ++ */ ++ @NotNull ++ default Collection getKeyedPluginChunkTicketKeys() { ++ return getWorld().getKeyedPluginChunkTicketKeys(getX(), getZ()); ++ } ++ // Paper end ++ + /** + * Gets the amount of time in ticks that this chunk has been inhabited. + * +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index e6d2abf284c103a8bcddd8b4f9cb34d86a4f2fa6..b94ea04f2ca9474e32a9cdeacd058310775ca884 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -39,6 +39,7 @@ import org.bukkit.util.Vector; + import org.jetbrains.annotations.Contract; + import org.jetbrains.annotations.NotNull; + import org.jetbrains.annotations.Nullable; ++import io.papermc.paper.world.TickingLevel; // Paper + + /** + * Represents a world, which may contain entities, chunks and blocks +@@ -1115,6 +1116,145 @@ public interface World extends PluginMessageRecipient, Metadatable { + @NotNull + public Map> getPluginChunkTickets(); + ++ // Paper start ++ /** ++ * Adds a {@link NamespacedKey} based plugin chunk ticket ++ * to the specified chunk with {@link TickingLevel#ENTITY}. ++ * If a ticket with a different level is already present, it will be removed. ++ * Asynchronously loads the specified {@link Chunk} if it isn't loaded. ++ * ++ * @param x X-coordinate of the chunk ++ * @param z Z-coordinate of the chunk ++ * @param key the identifier of the ticket ++ * @return the {@link TickingLevel} that was overwritten ++ * or {@code null} if no ticket was present with the specified key ++ * @see #addKeyedPluginChunkTicket(int, int, NamespacedKey, TickingLevel) ++ */ ++ @Nullable ++ TickingLevel addKeyedPluginChunkTicket(int x, int z, @NotNull NamespacedKey key); ++ ++ /** ++ * Adds a {@link NamespacedKey} based plugin chunk ticket ++ * to the specified chunk with the specified {@link TickingLevel}. ++ * If a ticket with a different level is already present, it will be removed. ++ * Asynchronously loads the specified {@link Chunk} if it isn't loaded. ++ * ++ * @param x X-coordinate of the chunk ++ * @param z Z-coordinate of the chunk ++ * @param key the identifier of the ticket ++ * @param level the level of the ticket ++ * @return the {@link TickingLevel} that was overwritten ++ * or {@code null} if no ticket was present with the specified key ++ */ ++ @Nullable ++ TickingLevel addKeyedPluginChunkTicket(int x, int z, @NotNull NamespacedKey key, @NotNull TickingLevel level); ++ ++ /** ++ * Removes the {@link NamespacedKey} based plugin chunk ticket ++ * from the specified chunk which has the specified key, if there is one. ++ * ++ * @param x X-coordinate of the chunk ++ * @param z Z-coordinate of the chunk ++ * @param key the identifier of the tickets ++ * @return the {@link TickingLevel} that was removed ++ * or {@code null} if no ticket was present with the specified key ++ */ ++ @Nullable ++ TickingLevel removeKeyedPluginChunkTicket(int x, int z, @NotNull NamespacedKey key); ++ ++ /** ++ * Gets the {@link NamespacedKey} based plugin chunk ticket level ++ * in the specified chunk with the specified key, if there is one. ++ * ++ * @param x X-coordinate of the chunk ++ * @param z Z-coordinate of the chunk ++ * @param key the identifier of the tickets ++ * @return the level which is present with the specified key or {@code null} if there is none ++ */ ++ @Nullable ++ TickingLevel getKeyedPluginChunkTicketLevel(int x, int z, @NotNull NamespacedKey key); ++ ++ /** ++ * Gets the keys of the {@link NamespacedKey} based plugin chunk tickets ++ * which are present in the specified chunk. ++ * ++ * @param x X-coordinate of the chunk ++ * @param z Z-coordinate of the chunk ++ * @return the keys which are present ++ */ ++ @NotNull ++ Collection getKeyedPluginChunkTicketKeys(int x, int z); ++ ++ /** ++ * Removes all {@link NamespacedKey} based plugin chunk tickets ++ * (from each chunk in this world) which are owned by the specified plugin ++ * according to {@link NamespacedKey#getNamespace()}. ++ * ++ * @param plugin the plugin whose tickets to remove ++ */ ++ void removeKeyedPluginChunkTickets(@NotNull Plugin plugin); ++ ++ /** ++ * Removes all {@link NamespacedKey} based plugin chunk tickets ++ * (from each chunk in this world) which have the specified key. ++ * ++ * @param key the identifier of the tickets which should be removed ++ */ ++ void removeKeyedPluginChunkTickets(@NotNull NamespacedKey key); ++ ++ /** ++ * Gets all chunk keys (which can be used in eg. {@link World#getChunkAt(long)}) ++ * which have a {@link NamespacedKey} based plugin chunk ticket with the specified key. ++ * Keep in mind that these chunks might not be loaded yet: ++ * having a chunk ticket only guarantees that chunk loading has begun, ++ * not that the loading has already been completed. ++ * If only loaded chunks are needed, please use ++ * {@link #getKeyedPluginChunkTicketLoadedChunks(NamespacedKey)} instead. ++ * ++ * @return the chunk keys which have tickets with the specified key ++ */ ++ @NotNull ++ Collection getKeyedPluginChunkTicketChunks(@NotNull NamespacedKey key); ++ ++ /** ++ * Gets all loaded chunks which have a {@link NamespacedKey} ++ * based plugin chunk ticket with the specified key. ++ * Keep in mind that only loaded chunks are included. ++ * If that's not desired, please use ++ * {@link #getKeyedPluginChunkTicketChunks(NamespacedKey)} instead. ++ * ++ * @return the loaded chunks which have tickets with the specified key ++ */ ++ @NotNull ++ Collection getKeyedPluginChunkTicketLoadedChunks(@NotNull NamespacedKey key); ++ ++ /** ++ * Gets all {@link NamespacedKey} based plugin chunk tickets ++ * and their chunk keys (which can be used in eg. {@link World#getChunkAt(long)}). ++ * The chunk keys point to the ticket keys they contain. ++ * Keep in mind that these chunks might not be loaded yet: ++ * having a chunk ticket only guarantees that chunk loading has begun, ++ * not that the loading has already been completed. ++ * If only loaded chunks are needed, please use ++ * {@link #getAllLoadedKeyedPluginChunkTickets()} instead. ++ * ++ * @return the chunk keys and their ticket keys ++ */ ++ @NotNull ++ com.google.common.collect.Multimap getAllKeyedPluginChunkTickets(); ++ ++ /** ++ * Gets all {@link NamespacedKey} based plugin chunk tickets whose chunks are loaded, ++ * and their chunks. The chunks point to the ticket keys they contain. ++ * Keep in mind that only chunks are included. ++ * If that's not desired, please use {@link #getAllKeyedPluginChunkTickets()} instead. ++ * ++ * @return the chunks and their ticket keys ++ */ ++ @NotNull ++ com.google.common.collect.Multimap getAllLoadedKeyedPluginChunkTickets(); ++ // Paper end ++ + /** + * Drops an item at the specified {@link Location} + * +diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index 26685f59b235ea5b4c4fb7ae21acb5149edaa2b3..4db22c112a3e81ca5d118403f3d6e502244ace94 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -551,6 +551,7 @@ public final class SimplePluginManager implements PluginManager { + try { + for (World world : server.getWorlds()) { + world.removePluginChunkTickets(plugin); ++ world.removeKeyedPluginChunkTickets(plugin); // Paper + } + } catch (Throwable ex) { + server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while removing chunk tickets for " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); diff --git a/Spigot-Server-Patches/0615-NamespacedKey-based-plugin-chunk-tickets.patch b/Spigot-Server-Patches/0615-NamespacedKey-based-plugin-chunk-tickets.patch new file mode 100644 index 000000000000..40fce7a568df --- /dev/null +++ b/Spigot-Server-Patches/0615-NamespacedKey-based-plugin-chunk-tickets.patch @@ -0,0 +1,285 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Trigary +Date: Sat, 22 Aug 2020 14:43:26 +0200 +Subject: [PATCH] NamespacedKey based plugin chunk tickets + + +diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java +index 3c7b225edbe23dc1959002293a6f8b816287b5a8..d733ff69d44a1ee10f3d7c41b92cd031dbccb035 100644 +--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java ++++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java +@@ -464,6 +464,34 @@ public abstract class ChunkMapDistance { + } + // CraftBukkit end + ++ // Paper start ++ public int removeIfAllTicketsFor(TicketType ticketType, java.util.function.Predicate> predicate) { ++ // logic copied from removeAllTicketsFor ++ int count = 0; ++ for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext(); ) { ++ Entry>> entry = iterator.next(); ++ ArraySetSorted> tickets = entry.getValue(); ++ for (java.util.Iterator> ticketIterator = tickets.iterator(); ticketIterator.hasNext(); ) { ++ Ticket ticket = ticketIterator.next(); ++ //noinspection unchecked ++ if (ticket.getTicketType() == ticketType && predicate.test((Ticket) ticket)) { ++ count++; ++ ticketIterator.remove(); ++ } ++ } ++ ++ // copied from removeTicket ++ this.ticketLevelTracker.update(entry.getLongKey(), getLowestTicketLevel(tickets), false); ++ ++ // can't use entry after it's removed ++ if (tickets.isEmpty()) { ++ iterator.remove(); ++ } ++ } ++ return count; ++ } ++ // Paper end ++ + class a extends ChunkMap { + + public a() { +diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java +index ff74be14512a947e81b62d53e616131ca7d7f609..736d00761b42bbd982707e7dc9b1d7c77eeb99d5 100644 +--- a/src/main/java/net/minecraft/server/MCUtil.java ++++ b/src/main/java/net/minecraft/server/MCUtil.java +@@ -5,6 +5,7 @@ import com.destroystokyo.paper.profile.CraftPlayerProfile; + import com.destroystokyo.paper.profile.PlayerProfile; + import com.google.common.util.concurrent.ThreadFactoryBuilder; + import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; ++import io.papermc.paper.world.TickingLevel; + import org.apache.commons.lang.exception.ExceptionUtils; + import com.google.gson.JsonArray; + import com.google.gson.JsonObject; +@@ -704,4 +705,31 @@ public final class MCUtil { + // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean) + return 33 + ChunkStatus.getTicketLevelOffset(status); + } ++ ++ public static int getTicketLevelFrom(TickingLevel tickingLevel) { ++ switch (tickingLevel) { ++ case ENTITY: ++ return 31; ++ case BLOCK: ++ return 32; ++ case NONE: ++ return 33; ++ default: ++ throw new AssertionError("Unhandled case: " + tickingLevel); ++ } ++ } ++ ++ @Nullable ++ public static TickingLevel tryGetTickingLevelFrom(int ticketLevel) { ++ switch (ticketLevel) { ++ case 31: ++ return TickingLevel.ENTITY; ++ case 32: ++ return TickingLevel.BLOCK; ++ case 33: ++ return TickingLevel.NONE; ++ default: ++ return null; ++ } ++ } + } +diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java +index 5c789b25f1df2eae8ea8ceb4ba977ba336fe6d5e..2cb8991b8d2b46478f17388a80fefde1091f6245 100644 +--- a/src/main/java/net/minecraft/server/TicketType.java ++++ b/src/main/java/net/minecraft/server/TicketType.java +@@ -22,6 +22,7 @@ public class TicketType { + public static final TicketType UNKNOWN = a("unknown", Comparator.comparingLong(ChunkCoordIntPair::pair), 1); + public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit ++ public static final TicketType KEYED_PLUGIN_TICKET = a("keyed_plugin_ticket", Comparator.comparing(org.bukkit.NamespacedKey::getNamespace).thenComparing(org.bukkit.NamespacedKey::getKey)); // Paper + public static final TicketType FUTURE_AWAIT = a("future_await", Long::compareTo); // Paper + public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper + public static final TicketType PRIORITY = a("priority", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index b71316cce3bdbf3485be456f0260c6b3463cff8e..88928a7c12c390c97614a467fb7478e6321129de 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -267,6 +267,7 @@ import org.bukkit.util.BoundingBox; + import org.bukkit.util.Consumer; + import org.bukkit.util.RayTraceResult; + import org.bukkit.util.Vector; ++import io.papermc.paper.world.TickingLevel; // Paper + + public class CraftWorld implements World { + public static final int CUSTOM_DIMENSION_OFFSET = 10; +@@ -717,6 +718,171 @@ public class CraftWorld implements World { + return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build())); + } + ++ // Paper start ++ @Override ++ public TickingLevel addKeyedPluginChunkTicket(int x, int z, org.bukkit.NamespacedKey key) { ++ return addKeyedPluginChunkTicket(x, z, key, TickingLevel.ENTITY); ++ } ++ ++ @Override ++ public TickingLevel addKeyedPluginChunkTicket(int x, int z, org.bukkit.NamespacedKey key, TickingLevel level) { ++ Preconditions.checkArgument(key != null, "null key"); ++ Preconditions.checkArgument(level != null, "null level"); ++ ++ TickingLevel previousLevel = getKeyedPluginChunkTicketLevel(x, z, key); ++ if (previousLevel == level) { ++ return level; //ticket already present at desired level -> do nothing ++ } ++ ++ if (previousLevel != null) { ++ //ticket with another level is present -> remove it first ++ // (only 1 level per chunk-key pair is allowed) ++ removeKeyedPluginChunkTicket(x, z, key); ++ } ++ ++ //finally actually add the ticket ++ int ticketLevel = MCUtil.getTicketLevelFrom(level); ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ manager.addTicketAtLevel(TicketType.KEYED_PLUGIN_TICKET, new ChunkCoordIntPair(x, z), ticketLevel, key); ++ return previousLevel; ++ } ++ ++ @Override ++ public TickingLevel removeKeyedPluginChunkTicket(int x, int z, org.bukkit.NamespacedKey key) { ++ Preconditions.checkArgument(key != null, "null key"); ++ ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ ChunkCoordIntPair coordPair = new ChunkCoordIntPair(x, z); ++ ++ for (TickingLevel level : TickingLevel.values()) { ++ int ticketLevel = MCUtil.getTicketLevelFrom(level); ++ if (manager.removeTicketAtLevel(TicketType.KEYED_PLUGIN_TICKET, coordPair, ticketLevel, key)) { ++ return level; ++ } ++ } ++ return null; ++ } ++ ++ @Override ++ public TickingLevel getKeyedPluginChunkTicketLevel(int x, int z, org.bukkit.NamespacedKey key) { ++ Preconditions.checkArgument(key != null, "null key"); ++ ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ Collection> tickets = manager.tickets.get(ChunkCoordIntPair.pair(x, z)); ++ if (tickets == null) { ++ return null; ++ } ++ ++ for (Ticket ticket : tickets) { ++ if (ticket.getTicketType() == TicketType.KEYED_PLUGIN_TICKET && key.equals(ticket.getObjectReason())) { ++ return MCUtil.tryGetTickingLevelFrom(ticket.getTicketLevel()); ++ } ++ } ++ return null; ++ } ++ ++ @Override ++ public Collection getKeyedPluginChunkTicketKeys(int x, int z) { ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ Collection> tickets = manager.tickets.get(ChunkCoordIntPair.pair(x, z)); ++ if (tickets == null) { ++ return Collections.emptyList(); ++ } ++ ++ com.google.common.collect.ImmutableSet.Builder ret = com.google.common.collect.ImmutableSet.builder(); ++ for (Ticket ticket : tickets) { ++ if (ticket.getTicketType() == TicketType.KEYED_PLUGIN_TICKET) { ++ ret.add((org.bukkit.NamespacedKey) ticket.getObjectReason()); ++ } ++ } ++ return ret.build(); ++ } ++ ++ @Override ++ public void removeKeyedPluginChunkTickets(Plugin plugin) { ++ Preconditions.checkArgument(plugin != null, "null plugin"); ++ ++ String namespace = new org.bukkit.NamespacedKey(plugin, "ignored").getNamespace(); ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ manager.removeIfAllTicketsFor(TicketType.KEYED_PLUGIN_TICKET, ++ ticket -> namespace.equals(ticket.getObjectReason().getNamespace())); ++ } ++ ++ @Override ++ public void removeKeyedPluginChunkTickets(org.bukkit.NamespacedKey key) { ++ Preconditions.checkArgument(key != null, "null key"); ++ ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ manager.removeIfAllTicketsFor(TicketType.KEYED_PLUGIN_TICKET, ++ ticket -> key.equals(ticket.getObjectReason())); ++ } ++ ++ @Override ++ public Collection getKeyedPluginChunkTicketChunks(org.bukkit.NamespacedKey key) { ++ Preconditions.checkArgument(key != null, "null key"); ++ ++ com.google.common.collect.ImmutableSet.Builder ret = com.google.common.collect.ImmutableSet.builder(); ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ ++ for (Long2ObjectMap.Entry>> chunkTickets : manager.tickets.long2ObjectEntrySet()) { ++ for (Ticket ticket : chunkTickets.getValue()) { ++ if (ticket.getTicketType() == TicketType.KEYED_PLUGIN_TICKET && key.equals(ticket.getObjectReason())) { ++ ret.add(chunkTickets.getLongKey()); ++ break; ++ } ++ } ++ } ++ return ret.build(); ++ } ++ ++ @Override ++ public Collection getKeyedPluginChunkTicketLoadedChunks(org.bukkit.NamespacedKey key) { ++ com.google.common.collect.ImmutableSet.Builder ret = com.google.common.collect.ImmutableSet.builder(); ++ for (long chunkKey : getKeyedPluginChunkTicketChunks(key)) { ++ int x = MCUtil.getCoordinateX(chunkKey); ++ int z = MCUtil.getCoordinateZ(chunkKey); ++ net.minecraft.server.Chunk chunk = world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(x, z); ++ if (chunk != null) { ++ ret.add(chunk.bukkitChunk); ++ } ++ } ++ return ret.build(); ++ } ++ ++ @Override ++ public com.google.common.collect.Multimap getAllKeyedPluginChunkTickets() { ++ com.google.common.collect.Multimap ret = com.google.common.collect.HashMultimap.create(); ++ ChunkMapDistance manager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; ++ ++ for (Long2ObjectMap.Entry>> chunkTickets : manager.tickets.long2ObjectEntrySet()) { ++ long chunkKey = chunkTickets.getLongKey(); ++ ++ for (Ticket ticket : chunkTickets.getValue()) { ++ if (ticket.getTicketType() == TicketType.KEYED_PLUGIN_TICKET) { ++ ret.put(chunkKey, (org.bukkit.NamespacedKey) ticket.getObjectReason()); ++ } ++ } ++ } ++ return ret; ++ } ++ ++ @Override ++ public com.google.common.collect.Multimap getAllLoadedKeyedPluginChunkTickets() { ++ com.google.common.collect.Multimap ret = com.google.common.collect.HashMultimap.create(); ++ com.google.common.collect.Multimap all = getAllKeyedPluginChunkTickets(); ++ ++ for (long chunkKey : all.keySet()) { ++ int x = MCUtil.getCoordinateX(chunkKey); ++ int z = MCUtil.getCoordinateZ(chunkKey); ++ net.minecraft.server.Chunk chunk = world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(x, z); ++ if (chunk != null) { ++ ret.putAll(chunk.bukkitChunk, all.get(chunkKey)); ++ } ++ } ++ return ret; ++ } ++ // Paper end ++ + @Override + public boolean isChunkForceLoaded(int x, int z) { + return getHandle().getForceLoadedChunks().contains(ChunkCoordIntPair.pair(x, z));