diff --git a/.gitignore b/.gitignore index 615b683..ed6498d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,7 @@ out/ # other stuff run/ -forktest-server -forktest-api +folia-server +folia-api !gradle/wrapper/gradle-wrapper.jar diff --git a/build.gradle.kts b/build.gradle.kts index ba3ffd2..7dee066 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,7 +50,7 @@ subprojects { } paperweight { - serverProject.set(project(":forktest-server")) + serverProject.set(project(":Folia-Server")) remapRepo.set(paperMavenPublicUrl) decompileRepo.set(paperMavenPublicUrl) @@ -58,10 +58,10 @@ paperweight { usePaperUpstream(providers.gradleProperty("paperRef")) { withPaperPatcher { apiPatchDir.set(layout.projectDirectory.dir("patches/api")) - apiOutputDir.set(layout.projectDirectory.dir("forktest-api")) + apiOutputDir.set(layout.projectDirectory.dir("Folia-API")) serverPatchDir.set(layout.projectDirectory.dir("patches/server")) - serverOutputDir.set(layout.projectDirectory.dir("forktest-server")) + serverOutputDir.set(layout.projectDirectory.dir("Folia-Server")) } } } @@ -71,20 +71,20 @@ paperweight { // tasks.generateDevelopmentBundle { - apiCoordinates.set("com.example.paperfork:forktest-api") + apiCoordinates.set("dev.folia:folia-api") mojangApiCoordinates.set("io.papermc.paper:paper-mojangapi") libraryRepositories.set( listOf( "https://repo.maven.apache.org/maven2/", paperMavenPublicUrl, - // "https://my.repo/", // This should be a repo hosting your API (in this example, 'com.example.paperfork:forktest-api') + // "https://my.repo/", // This should be a repo hosting your API (in this example, 'dev.folia:folia-api') ) ) } allprojects { // Publishing API: - // ./gradlew :ForkTest-API:publish[ToMavenLocal] + // ./gradlew :folia-API:publish[ToMavenLocal] publishing { repositories { maven { diff --git a/gradle.properties b/gradle.properties index 829d5ad..ed67948 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ -group=com.example.paperfork +group=dev.folia version=1.19.3-R0.1-SNAPSHOT mcVersion=1.19.3 -paperRef=adb8e499dbc6050abf4a690d369cf506bc3ac318 +paperRef=4da844f1e3e375a24a0e518b0787ae909fa0e247 org.gradle.caching=true org.gradle.parallel=true diff --git a/patches/server/0001-Build-changes.patch b/patches/server/0001-Build-changes.patch index 18163fc..ad4a197 100644 --- a/patches/server/0001-Build-changes.patch +++ b/patches/server/0001-Build-changes.patch @@ -1,11 +1,11 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MiniDigger -Date: Sat, 12 Jun 2021 16:40:34 +0200 +From: Spottedleaf +Date: Thu, 23 Feb 2023 07:56:29 -0800 Subject: [PATCH] Build changes diff --git a/build.gradle.kts b/build.gradle.kts -index d5d49bb2b47c889e12d17dc87b8c439a60b3fe67..497db79710a93e18c245ba8ac5853dd5ac6012b5 100644 +index 781609605d25283009e5f3e61649ecde9ea9a4cb..0a389871b8bd66252455773a6d735576a1bfcd77 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,8 +7,12 @@ plugins { @@ -14,12 +14,12 @@ index d5d49bb2b47c889e12d17dc87b8c439a60b3fe67..497db79710a93e18c245ba8ac5853dd5 dependencies { - implementation(project(":paper-api")) - implementation(project(":paper-mojangapi")) -+ // ForkTest start -+ implementation(project(":forktest-api")) ++ // Folia start ++ implementation(project(":Folia-API")) + implementation("io.papermc.paper:paper-mojangapi:1.19.3-R0.1-SNAPSHOT") { + exclude("io.papermc.paper", "paper-api") + } -+ // ForkTest end ++ // Folia end // Paper start implementation("org.jline:jline-terminal-jansi:3.21.0") implementation("net.minecrell:terminalconsoleappender:1.3.0") @@ -28,7 +28,7 @@ index d5d49bb2b47c889e12d17dc87b8c439a60b3fe67..497db79710a93e18c245ba8ac5853dd5 "Main-Class" to "org.bukkit.craftbukkit.Main", "Implementation-Title" to "CraftBukkit", - "Implementation-Version" to "git-Paper-$implementationVersion", -+ "Implementation-Version" to "git-ForkTest-$implementationVersion", // ForkTest ++ "Implementation-Version" to "git-Folia-$implementationVersion", // Folia "Implementation-Vendor" to date, // Paper "Specification-Title" to "Bukkit", "Specification-Version" to project.version, @@ -37,12 +37,12 @@ index d5d49bb2b47c889e12d17dc87b8c439a60b3fe67..497db79710a93e18c245ba8ac5853dd5 block: JavaExec.() -> Unit ): TaskProvider = register(name) { - group = "paper" -+ group = "paperweight" // ForkTest ++ group = "paperweight" // Folia mainClass.set("org.bukkit.craftbukkit.Main") standardInput = System.`in` workingDir = rootProject.layout.projectDirectory diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 710ca7d3a5659953f64bc6dccdd93b43300961cc..57e0aa0341b359442e562ef4e213b1c785841788 100644 +index 710ca7d3a5659953f64bc6dccdd93b43300961cc..2ee4e5e8d17a3a1e6a342c74b13135df030ffef6 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1654,7 +1654,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop // Spigot - Spigot > // CraftBukkit - cb > vanilla! -+ return "ForkTest"; // ForkTest - ForkTest > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! ++ return "Folia"; // Folia - Folia > // Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! } public SystemReport fillSystemReport(SystemReport details) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 47df6f4268a63118da8187f4102c876bd37d1680..24a3c5228fe22683bc87c0c6251a9e49b9426ad7 100644 +index bfc4ee36befb925ab4eb6b96f5c1aa6c76bf711f..2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -261,7 +261,7 @@ import javax.annotation.Nullable; // Paper @@ -63,12 +63,12 @@ index 47df6f4268a63118da8187f4102c876bd37d1680..24a3c5228fe22683bc87c0c6251a9e49 public final class CraftServer implements Server { - private final String serverName = "Paper"; // Paper -+ private final String serverName = "ForkTest"; // ForkTest // Paper ++ private final String serverName = "Folia"; // Folia // Paper private final String serverVersion; private final String bukkitVersion = Versioning.getBukkitVersion(); private final Logger logger = Logger.getLogger("Minecraft"); diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java -index 774556a62eb240da42e84db4502e2ed43495be17..21f39bd0c33ef2635249298e6a247afba8b05742 100644 +index 774556a62eb240da42e84db4502e2ed43495be17..e9b6ca3aa25e140467ae866d572483050ea3fa0e 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java @@ -11,7 +11,7 @@ public final class Versioning { @@ -76,7 +76,7 @@ index 774556a62eb240da42e84db4502e2ed43495be17..21f39bd0c33ef2635249298e6a247afb String result = "Unknown-Version"; - InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/io.papermc.paper/paper-api/pom.properties"); -+ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.example.paperfork/forktest-api/pom.properties"); // ForkTest ++ InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/dev.folia/folia-api/pom.properties"); // Folia Properties properties = new Properties(); if (stream != null) { diff --git a/patches/server/0002-New-player-chunk-loader-system.patch b/patches/server/0002-New-player-chunk-loader-system.patch new file mode 100644 index 0000000..ea73192 --- /dev/null +++ b/patches/server/0002-New-player-chunk-loader-system.patch @@ -0,0 +1,2281 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Wed, 1 Feb 2023 21:06:31 -0800 +Subject: [PATCH] New player chunk loader system + + +diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java +index 06bff37e4c1fddd3be6343049a66787c63fb420c..1be1fe766401221b6adb417175312007d29d347e 100644 +--- a/src/main/java/co/aikar/timings/TimingsExport.java ++++ b/src/main/java/co/aikar/timings/TimingsExport.java +@@ -163,9 +163,9 @@ public class TimingsExport extends Thread { + return pair(rule, world.getWorld().getGameRuleValue(rule)); + })), + // Paper start - replace chunk loader system +- pair("ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()), +- pair("no-ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()), +- pair("sending-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance()) ++ pair("ticking-distance", world.getWorld().getSimulationDistance()), ++ pair("no-ticking-distance", world.getWorld().getViewDistance()), ++ pair("sending-distance", world.getWorld().getSendViewDistance()) + // Paper end - replace chunk loader system + )); + })); +diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +index 0e45a340ae534caf676b7f9d0adcbcee5829925e..6df1948b1204a7288ecb7238b6fc2a733f7d25b3 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java ++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +@@ -129,15 +129,15 @@ public final class ChunkSystem { + } + + public static int getSendViewDistance(final ServerPlayer player) { +- return io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPISendViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { +- return io.papermc.paper.chunk.PlayerChunkLoader.getLoadViewDistance(player); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getLoadViewDistance(player); + } + + public static int getTickViewDistance(final ServerPlayer player) { +- return io.papermc.paper.chunk.PlayerChunkLoader.getTickViewDistance(player); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPITickViewDistance(player); + } + + private ChunkSystem() { +diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a4d58352eebed11fafde8c381afe3572893b8f8f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java +@@ -0,0 +1,1302 @@ ++package io.papermc.paper.chunk.system; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.IntegerUtil; ++import io.papermc.paper.util.IntervalledCounter; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; ++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.GameRules; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.levelgen.BelowZeroRetrogen; ++import org.apache.commons.lang3.mutable.MutableObject; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++ ++import java.util.ArrayDeque; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public class RegionisedPlayerChunkLoader { ++ ++ public static final TicketType REGION_PLAYER_TICKET = TicketType.create("region_player_ticket", Long::compareTo); ++ ++ public static final int MIN_VIEW_DISTANCE = 2; ++ public static final int MAX_VIEW_DISTANCE = 32; ++ ++ public static final int TICK_TICKET_LEVEL = 31; ++ public static final int GENERATED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL); ++ public static final int LOADED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.EMPTY); ++ ++ public static final record ViewDistances( ++ int tickViewDistance, ++ int loadViewDistance, ++ int sendViewDistance ++ ) { ++ public ViewDistances setTickViewDistance(final int distance) { ++ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setLoadViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); ++ } ++ ++ ++ public ViewDistances setSendViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); ++ } ++ } ++ ++ public static int getAPITickViewDistance(final Player player) { ++ return getAPITickViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPITickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPITickDistance(); ++ } ++ return data.lastTickDistance; ++ } ++ ++ public static int getAPIViewDistance(final Player player) { ++ return getAPIViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPIViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getAPISendViewDistance(final Player player) { ++ return getAPISendViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPISendViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPISendViewDistance(); ++ } ++ return data.lastSendDistance; ++ } ++ ++ private final ServerLevel world; ++ ++ public RegionisedPlayerChunkLoader(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ ++ if (player.chunkLoader != null) { ++ throw new IllegalStateException("Player is already added to player chunk loader"); ++ } ++ ++ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); ++ ++ player.chunkLoader = loader; ++ loader.add(); ++ } ++ ++ public void updatePlayer(final ServerPlayer player) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader != null) { ++ loader.update(); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ ++ if (loader == null) { ++ throw new IllegalStateException("Player is already removed from player chunk loader"); ++ } ++ ++ loader.remove(); ++ player.chunkLoader = null; ++ } ++ ++ public void setSendDistance(final int distance) { ++ this.world.setSendViewDistance(distance); ++ } ++ ++ public void setLoadDistance(final int distance) { ++ this.world.setLoadViewDistance(distance); ++ } ++ ++ public void setTickDistance(final int distance) { ++ this.world.setTickViewDistance(distance); ++ } ++ ++ // Note: follow the player chunk loader so everything stays consistent... ++ public int getAPITickDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ return tickViewDistance; ++ } ++ ++ public int getAPIViewDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ ++ // loadDistance = api view distance + 1 ++ return loadDistance - 1; ++ } ++ ++ public int getAPISendViewDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( ++ loadDistance, -1, -1, distances.sendViewDistance ++ ); ++ ++ return sendViewDistance; ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { ++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader == null) { ++ return false; ++ } ++ ++ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader == null) { ++ return false; ++ } ++ ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public void tick() { ++ TickThread.ensureTickThread("Cannot tick player chunk loader async"); ++ for (final ServerPlayer player : this.world.players()) { ++ player.chunkLoader.update(); ++ } ++ } ++ ++ public void tickMidTick() { ++ final long time = System.nanoTime(); ++ for (final ServerPlayer player : this.world.players()) { ++ player.chunkLoader.midTickUpdate(time); ++ } ++ } ++ ++ private static long[] generateBFSOrder(final int radius) { ++ final LongArrayList chunks = new LongArrayList(); ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ ++ seen.add(CoordinateUtils.getChunkKey(0, 0)); ++ queue.enqueue(CoordinateUtils.getChunkKey(0, 0)); ++ while (!queue.isEmpty()) { ++ final long chunk = queue.dequeueLong(); ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ ++ // important that the addition to the list is here, rather than when enqueueing neighbours ++ // ensures the order is actually kept ++ chunks.add(chunk); ++ ++ // -x ++ final long n1 = CoordinateUtils.getChunkKey(chunkX - 1, chunkZ); ++ // -z ++ final long n2 = CoordinateUtils.getChunkKey(chunkX, chunkZ - 1); ++ // +x ++ final long n3 = CoordinateUtils.getChunkKey(chunkX + 1, chunkZ); ++ // +z ++ final long n4 = CoordinateUtils.getChunkKey(chunkX, chunkZ + 1); ++ ++ final long[] list = new long[] {n1, n2, n3, n4}; ++ ++ for (final long neighbour : list) { ++ final int neighbourX = CoordinateUtils.getChunkX(neighbour); ++ final int neighbourZ = CoordinateUtils.getChunkZ(neighbour); ++ if (Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { ++ // don't enqueue out of range ++ continue; ++ } ++ if (!seen.add(neighbour)) { ++ continue; ++ } ++ queue.enqueue(neighbour); ++ } ++ } ++ ++ return chunks.toLongArray(); ++ } ++ ++ public static final class PlayerChunkLoaderData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ private final long id = ID_GENERATOR.incrementAndGet(); ++ private final Long idBoxed = Long.valueOf(this.id); ++ ++ // expected that this list returns for a given radius, the set of chunks ordered ++ // by manhattan distance ++ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[65][]; ++ static { ++ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { ++ // a BFS around -x, -z, +x, +z will give increasing manhatten distance ++ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); ++ } ++ } ++ ++ private final ServerPlayer player; ++ private final ServerLevel world; ++ ++ private int lastChunkX = Integer.MIN_VALUE; ++ private int lastChunkZ = Integer.MIN_VALUE; ++ ++ private int lastSendDistance = Integer.MIN_VALUE; ++ private int lastLoadDistance = Integer.MIN_VALUE; ++ private int lastTickDistance = Integer.MIN_VALUE; ++ ++ private int lastSentChunkCenterX = Integer.MIN_VALUE; ++ private int lastSentChunkCenterZ = Integer.MIN_VALUE; ++ ++ private int lastSentChunkRadius = Integer.MIN_VALUE; ++ private int lastSentSimulationDistance = Integer.MIN_VALUE; ++ ++ private boolean canGenerateChunks = true; ++ ++ private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); ++ private final LongOpenHashSet sentChunks = new LongOpenHashSet(); ++ ++ private static final byte CHUNK_TICKET_STAGE_NONE = 0; ++ private static final byte CHUNK_TICKET_STAGE_LOADING = 1; ++ private static final byte CHUNK_TICKET_STAGE_LOADED = 2; ++ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; ++ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; ++ private static final byte CHUNK_TICKET_STAGE_TICK = 5; ++ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { ++ ChunkHolderManager.MAX_TICKET_LEVEL + 1, ++ LOADED_TICKET_LEVEL, ++ LOADED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ TICK_TICKET_LEVEL ++ }; ++ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); ++ { ++ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); ++ } ++ ++ // rate limiting ++ private final MultiIntervalledCounter chunkSendCounter = new MultiIntervalledCounter( ++ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) ++ ); ++ private final MultiIntervalledCounter chunkLoadTicketCounter = new MultiIntervalledCounter( ++ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) ++ ); ++ private final MultiIntervalledCounter chunkGenerateTicketCounter = new MultiIntervalledCounter( ++ TimeUnit.MILLISECONDS.toNanos(50L), TimeUnit.MILLISECONDS.toNanos(250L), TimeUnit.SECONDS.toNanos(1L) ++ ); ++ ++ // queues ++ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { ++ final int c1x = CoordinateUtils.getChunkX(c1); ++ final int c1z = CoordinateUtils.getChunkZ(c1); ++ ++ final int c2x = CoordinateUtils.getChunkX(c2); ++ final int c2z = CoordinateUtils.getChunkZ(c2); ++ ++ final int centerX = PlayerChunkLoaderData.this.lastChunkX; ++ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; ++ ++ return Integer.compare( ++ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), ++ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) ++ ); ++ }; ++ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ ++ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { ++ this.world = world; ++ this.player = player; ++ } ++ ++ private void flushDelayedTicketOps() { ++ if (this.delayedTicketOps.isEmpty()) { ++ return; ++ } ++ this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps); ++ this.delayedTicketOps.clear(); ++ this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates(); ++ } ++ ++ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { ++ this.delayedTicketOps.addLast(op); ++ } ++ ++ private void sendChunk(final int chunkX, final int chunkZ) { ++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ this.world.getChunkSource().chunkMap.updateChunkTracking(this.player, ++ new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded ++ return; ++ } ++ throw new IllegalStateException(); ++ } ++ ++ private void sendUnloadChunk(final int chunkX, final int chunkZ) { ++ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ return; ++ } ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { ++ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, ++ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded ++ } ++ ++ private final SingleUserAreaMap broadcastMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ parameter.sendUnloadChunk(chunkX, chunkZ); ++ } ++ }; ++ private final SingleUserAreaMap loadTicketCleanup = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final byte ticketStage = parameter.chunkTicketStage.remove(chunk); ++ final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; ++ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ TicketType.UNKNOWN, level, new ChunkPos(chunkX, chunkZ), ++ REGION_PLAYER_TICKET, level, parameter.idBoxed ++ )); ++ } ++ }; ++ private final SingleUserAreaMap tickMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we will detect ticking chunks when we try to load them ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at ++ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated ++ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { ++ return; ++ } ++ ++ // Since we are possibly downgrading the ticket level, we add an unknown ticket so that ++ // the level is kept until tick(). ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed ++ )); ++ // keep chunk at new generated level ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( ++ chunk, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed ++ )); ++ } ++ }; ++ ++ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, ++ final int sendRadius) { ++ // expect sendRadius to be = 1 + target viewable radius ++ return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius); ++ } ++ ++ private static int getClientViewDistance(final ServerPlayer player) { ++ final Integer vd = player.clientViewDistance; ++ return vd == null ? -1 : Math.max(0, vd.intValue()); ++ } ++ ++ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) { ++ return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance; ++ } ++ ++ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, ++ final int worldLoadViewDistance) { ++ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); ++ } ++ ++ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, ++ final int playerSendViewDistance, final int worldSendViewDistance) { ++ return Math.min( ++ loadViewDistance, ++ playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? loadViewDistance : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance ++ ); ++ } ++ ++ private Packet updateClientChunkRadius(final int radius) { ++ this.lastSentChunkRadius = radius; ++ return new ClientboundSetChunkCacheRadiusPacket(radius); ++ } ++ ++ private Packet updateClientSimulationDistance(final int distance) { ++ this.lastSentSimulationDistance = distance; ++ return new ClientboundSetSimulationDistancePacket(distance); ++ } ++ ++ private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { ++ this.lastSentChunkCenterX = chunkX; ++ this.lastSentChunkCenterZ = chunkZ; ++ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); ++ } ++ ++ private boolean canPlayerGenerateChunks() { ++ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); ++ } ++ ++ private int getMaxChunkLoads() { ++ final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1); ++ int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; ++ if (configLimit == 0) { ++ // by default, only allow 1/10th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5, radiusChunks / 10); ++ } else if (configLimit < 0) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.loadingQueue.size(); ++ ++ int rateLimit; ++ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; ++ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { ++ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure ++ // there are no issues with the cast to integer ++ rateLimit = Integer.MAX_VALUE; ++ } else { ++ rateLimit = (int)this.chunkLoadTicketCounter.getMaxCountBeforeViolation(configRate); ++ } ++ ++ return Math.min(configLimit, rateLimit); ++ } ++ ++ private int getMaxChunkGenerates() { ++ final int radiusChunks = (2 * this.lastLoadDistance + 1) * (2 * this.lastLoadDistance + 1); ++ int configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; ++ if (configLimit == 0) { ++ // by default, only allow 1/10th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5, radiusChunks / 10); ++ } else if (configLimit < 0) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.generatingQueue.size(); ++ ++ int rateLimit; ++ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; ++ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { ++ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure ++ // there are no issues with the cast to integer ++ rateLimit = Integer.MAX_VALUE; ++ } else { ++ rateLimit = (int)this.chunkGenerateTicketCounter.getMaxCountBeforeViolation(configRate); ++ } ++ ++ return Math.min(configLimit, rateLimit); ++ } ++ ++ private int getMaxChunkSends() { ++ final int radiusChunks = (2 * this.lastSendDistance + 1) * (2 * this.lastSendDistance + 1); ++ ++ int rateLimit; ++ double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; ++ if (configRate < 0.0 || configRate > (1000.0 * (double)radiusChunks)) { ++ // getMaxCountBeforeViolation may not work with large config rates, so by checking against the load count we ensure ++ // there are no issues with the cast to integer ++ rateLimit = Integer.MAX_VALUE; ++ } else { ++ rateLimit = (int)this.chunkSendCounter.getMaxCountBeforeViolation(configRate); ++ } ++ ++ return rateLimit; ++ } ++ ++ private boolean wantChunkSent(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastSendDistance && wantChunkLoaded( ++ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance ++ ); ++ } ++ ++ private boolean wantChunkTicked(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; ++ } ++ ++ void midTickUpdate(final long time) { ++ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); ++ // update rate limits ++ this.chunkSendCounter.update(time); ++ this.chunkGenerateTicketCounter.update(time); ++ this.chunkLoadTicketCounter.update(time); ++ ++ // try to progress chunk loads ++ while (!this.loadingQueue.isEmpty()) { ++ final long pendingLoadChunk = this.loadingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); ++ final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ // chunk has loaded, so we can take it out of the queue ++ this.loadingQueue.dequeueLong(); ++ ++ // try to move to generate queue ++ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); ++ if (prev != CHUNK_TICKET_STAGE_LOADING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); ++ } ++ ++ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { ++ this.genQueue.enqueue(pendingLoadChunk); ++ } // else: don't want to generate, so just leave it loaded ++ } ++ ++ // try to push more chunk loads ++ int loadSlots; ++ while ((loadSlots = Math.min(this.getMaxChunkLoads(), this.loadQueue.size())) > 0) { ++ final LongArrayList chunks = new LongArrayList(loadSlots); ++ int actualLoadsQueued = 0; ++ for (int i = 0; i < loadSlots; ++i) { ++ final long chunk = this.loadQueue.dequeueLong(); ++ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); ++ if (prev != CHUNK_TICKET_STAGE_NONE) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addOp( ++ chunk, ++ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ chunks.add(chunk); ++ this.loadingQueue.enqueue(chunk); ++ ++ if (this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)) == null) { ++ // this is a good enough approximation for counting, but NOT for actual state management ++ ++actualLoadsQueued; ++ } ++ } ++ if (actualLoadsQueued > 0) { ++ this.chunkLoadTicketCounter.addTime(time, (long)actualLoadsQueued); ++ } ++ ++ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false ++ this.flushDelayedTicketOps(); ++ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk ++ // load - only generate ticket levels start anything, but they start generation... ++ // propagate levels ++ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked ++ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); ++ ++ for (int i = 0; i < loadSlots; ++i) { ++ final long queuedLoadChunk = chunks.getLong(i); ++ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); ++ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); ++ this.world.chunkTaskScheduler.scheduleChunkLoad( ++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null ++ ); ++ } ++ } ++ ++ // try to progress chunk generations ++ while (!this.generatingQueue.isEmpty()) { ++ final long pendingGenChunk = this.generatingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); ++ final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ ++ // chunk has generated, so we can take it out of queue ++ this.generatingQueue.dequeueLong(); ++ ++ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); ++ if (prev != CHUNK_TICKET_STAGE_GENERATING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); ++ } ++ ++ // try to move to send queue ++ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { ++ this.sendQueue.enqueue(pendingGenChunk); ++ } ++ // try to move to tick queue ++ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { ++ this.tickingQueue.enqueue(pendingGenChunk); ++ } ++ } ++ ++ // try to push more chunk generations ++ int genSlots; ++ while ((genSlots = Math.min(this.getMaxChunkGenerates(), this.genQueue.size())) > 0) { ++ int actualGenerationsQueued = 0; ++ for (int i = 0; i < genSlots; ++i) { ++ final long chunk = this.genQueue.dequeueLong(); ++ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_GENERATING); ++ if (prev != CHUNK_TICKET_STAGE_LOADED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, ++ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ this.generatingQueue.enqueue(chunk); ++ final ChunkAccess existingChunk = this.world.chunkSource.getChunkAtImmediately(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); ++ if (existingChunk == null || !existingChunk.getStatus().isOrAfter(ChunkStatus.FULL)) { ++ // this is a good enough approximation for counting, but NOT for actual state management ++ ++actualGenerationsQueued; ++ } ++ } ++ if (actualGenerationsQueued > 0) { ++ this.chunkGenerateTicketCounter.addTime(time, (long)actualGenerationsQueued); ++ } ++ } ++ ++ // try to pull ticking chunks ++ tick_check_outer: ++ while (!this.tickingQueue.isEmpty()) { ++ final long pendingTicking = this.tickingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); ++ ++ final int tickingReq = 2; ++ for (int dz = -tickingReq; dz <= tickingReq; ++dz) { ++ for (int dx = -tickingReq; dx <= tickingReq; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ); ++ final byte stage = this.chunkTicketStage.get(neighbour); ++ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { ++ break tick_check_outer; ++ } ++ } ++ } ++ // only gets here if all neighbours were marked as generated or ticking themselves ++ this.tickingQueue.dequeueLong(); ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ pendingTicking, ++ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ // there is no queue to add after ticking ++ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); ++ if (prev != CHUNK_TICKET_STAGE_GENERATED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); ++ } ++ } ++ ++ // try to pull sending chunks ++ final int maxSends = this.getMaxChunkSends(); ++ final int sendSlots = Math.min(maxSends, this.sendQueue.size()); ++ for (int i = 0; i < sendSlots; ++i) { ++ final long pendingSend = this.sendQueue.firstLong(); ++ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); ++ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); ++ final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ); ++ if (!chunk.areNeighboursLoaded(1)) { ++ // nothing to do ++ break; ++ } ++ this.sendQueue.dequeueLong(); ++ ++ this.sendChunk(pendingSendX, pendingSendZ); ++ } ++ if (sendSlots > 0) { ++ this.chunkSendCounter.addTime(time, sendSlots); ++ } ++ ++ this.flushDelayedTicketOps(); ++ // we assume propagate ticket levels happens after this call ++ } ++ ++ void add() { ++ final ViewDistances playerDistances = this.player.getViewDistances(); ++ final ViewDistances worldDistances = this.world.getViewDistances(); ++ final int chunkX = this.player.chunkPosition().x; ++ final int chunkZ = this.player.chunkPosition().z; ++ ++ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ // send view distances ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ ++ // add to distance maps ++ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance); ++ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); ++ this.tickMap.add(chunkX, chunkZ, tickViewDistance); ++ ++ // update chunk center ++ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); ++ ++ // now we can update ++ this.update(); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { ++ return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ)); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { ++ final BelowZeroRetrogen belowZeroRetrogen; ++ return chunkAccess != null && ( ++ chunkAccess.getStatus() == ChunkStatus.FULL || ++ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.FULL)) ++ ); ++ } ++ ++ void update() { ++ final ViewDistances playerDistances = this.player.getViewDistances(); ++ final ViewDistances worldDistances = this.world.getViewDistances(); ++ ++ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ final ChunkPos playerPos = this.player.chunkPosition(); ++ final boolean canGenerateChunks = this.canPlayerGenerateChunks(); ++ final int currentChunkX = playerPos.x; ++ final int currentChunkZ = playerPos.z; ++ ++ final int prevChunkX = this.lastChunkX; ++ final int prevChunkZ = this.lastChunkZ; ++ ++ if ( ++ // has view distance stayed the same? ++ sendViewDistance == this.lastSendDistance ++ && loadViewDistance == this.lastLoadDistance ++ && tickViewDistance == this.lastTickDistance ++ ++ // has our chunk stayed the same? ++ && prevChunkX == currentChunkX ++ && prevChunkZ == currentChunkZ ++ ++ // can we still generate chunks? ++ && this.canGenerateChunks == canGenerateChunks ++ ) { ++ // nothing we care about changed, so we're not re-calculating ++ return; ++ } ++ ++ // update distance maps ++ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance); ++ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); ++ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); ++ if (sendViewDistance > loadViewDistance || tickViewDistance > (loadViewDistance - 1)) { ++ throw new IllegalStateException(); ++ } ++ ++ // update VDs for client ++ // this should be after the distance map updates, as they will send unload packets ++ if (this.lastSentChunkRadius != sendViewDistance) { ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ } ++ if (this.lastSentSimulationDistance != tickViewDistance) { ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ } ++ ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ this.lastChunkX = currentChunkX; ++ this.lastChunkZ = currentChunkZ; ++ this.lastSendDistance = sendViewDistance; ++ this.lastLoadDistance = loadViewDistance; ++ this.lastTickDistance = tickViewDistance; ++ this.canGenerateChunks = canGenerateChunks; ++ ++ // +1 since we need to load chunks +1 around the load view distance... ++ final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1]; ++ // the iteration order is by increasing manhattan distance - so, we do NOT need to ++ // sort anything in the queue! ++ for (final long deltaChunk : toIterate) { ++ final int dx = CoordinateUtils.getChunkX(deltaChunk); ++ final int dz = CoordinateUtils.getChunkZ(deltaChunk); ++ final int chunkX = dx + currentChunkX; ++ final int chunkZ = dz + currentChunkZ; ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); ++ ++ // since chunk sending is not by radius alone, we need an extra check here to account for ++ // everything <= sendDistance ++ // Note: Vanilla may want to send chunks outside the send view distance, so we do need ++ // the dist <= view check ++ final boolean sendChunk = squareDistance <= sendViewDistance ++ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); ++ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); ++ ++ if (!sendChunk && sentChunk) { ++ // have sent the chunk, but don't want it anymore ++ // unload it now ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ final byte stage = this.chunkTicketStage.get(chunk); ++ switch (stage) { ++ case CHUNK_TICKET_STAGE_NONE: { ++ // we want the chunk to be at least loaded ++ this.loadQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADING: { ++ this.loadingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADED: { ++ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { ++ this.genQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATING: { ++ this.generatingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATED: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ if (squareDistance <= tickViewDistance) { ++ this.tickingQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_TICK: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown stage: " + stage); ++ } ++ } ++ } ++ ++ // update the chunk center ++ // this must be done last so that the client does not ignore any of our unload chunk packets above ++ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { ++ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); ++ } ++ ++ this.flushDelayedTicketOps(); ++ } ++ ++ void remove() { ++ // sends the chunk unload packets ++ this.broadcastMap.remove(); ++ // cleans up loading/generating tickets ++ this.loadTicketCleanup.remove(); ++ // cleans up ticking tickets ++ this.tickMap.remove(); ++ ++ // purge queues ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ // flush ticket changes ++ this.flushDelayedTicketOps(); ++ ++ // now all tickets should be removed, which is all of our external state ++ } ++ } ++ ++ private static final class MultiIntervalledCounter { ++ ++ private final IntervalledCounter[] counters; ++ ++ public MultiIntervalledCounter(final long... intervals) { ++ final IntervalledCounter[] counters = new IntervalledCounter[intervals.length]; ++ for (int i = 0; i < intervals.length; ++i) { ++ counters[i] = new IntervalledCounter(intervals[i]); ++ } ++ this.counters = counters; ++ } ++ ++ public long getMaxCountBeforeViolation(final double rate) { ++ long count = Long.MAX_VALUE; ++ for (final IntervalledCounter counter : this.counters) { ++ final long sum = counter.getSum(); ++ final long interval = counter.getInterval(); ++ // rate = sum / interval ++ // so, sum = rate*interval ++ final long maxSum = (long)Math.floor(rate * (1.0E-9 * (double)interval)); ++ final long diff = maxSum - sum; ++ if (diff < count) { ++ count = diff; ++ } ++ } ++ ++ return Math.max(0L, count); ++ } ++ ++ public void update(final long time) { ++ for (final IntervalledCounter counter : this.counters) { ++ counter.updateCurrentTime(time); ++ } ++ } ++ ++ public void updateAndAdd(final long count, final long time) { ++ for (final IntervalledCounter counter : this.counters) { ++ counter.updateAndAdd(count, time); ++ } ++ } ++ ++ public void addTime(final long time, final long count) { ++ for (final IntervalledCounter counter : this.counters) { ++ counter.addTime(time, count); ++ } ++ } ++ ++ public double getMaxRate() { ++ double ret = 0.0; ++ ++ for (final IntervalledCounter counter : this.counters) { ++ final double counterRate = counter.getRate(); ++ if (counterRate > ret) { ++ ret = counterRate; ++ } ++ } ++ ++ return ret; ++ } ++ } ++ ++ // TODO rebase into util patch ++ public static abstract class SingleUserAreaMap { ++ ++ private static final int NOT_SET = Integer.MIN_VALUE; ++ ++ private final T parameter; ++ private int lastChunkX = NOT_SET; ++ private int lastChunkZ = NOT_SET; ++ private int distance = NOT_SET; ++ ++ public SingleUserAreaMap(final T parameter) { ++ this.parameter = parameter; ++ } ++ ++ /* math sign function except 0 returns 1 */ ++ protected static int sign(int val) { ++ return 1 | (val >> (Integer.SIZE - 1)); ++ } ++ ++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.addCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.removeCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ public final boolean add(final int chunkX, final int chunkZ, final int distance) { ++ if (distance < 0) { ++ throw new IllegalArgumentException(Integer.toString(distance)); ++ } ++ if (this.lastChunkX != NOT_SET) { ++ return false; ++ } ++ this.lastChunkX = chunkX; ++ this.lastChunkZ = chunkZ; ++ this.distance = distance; ++ ++ this.addToNew(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++ ++ public final boolean update(final int toX, final int toZ, final int newViewDistance) { ++ if (newViewDistance < 0) { ++ throw new IllegalArgumentException(Integer.toString(newViewDistance)); ++ } ++ final int fromX = this.lastChunkX; ++ final int fromZ = this.lastChunkZ; ++ final int oldViewDistance = this.distance; ++ if (fromX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = toX; ++ this.lastChunkZ = toZ; ++ ++ final T parameter = this.parameter; ++ ++ ++ final int dx = toX - fromX; ++ final int dz = toZ - fromZ; ++ ++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX); ++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { ++ // teleported? ++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance); ++ this.addToNew(parameter, toX, toZ, newViewDistance); ++ return true; ++ } ++ ++ if (oldViewDistance != newViewDistance) { ++ // remove loop ++ ++ final int oldMinX = fromX - oldViewDistance; ++ final int oldMinZ = fromZ - oldViewDistance; ++ final int oldMaxX = fromX + oldViewDistance; ++ final int oldMaxZ = fromZ + oldViewDistance; ++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) { ++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { ++ ++ // only remove if we're outside the new view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ // add loop ++ ++ final int newMinX = toX - newViewDistance; ++ final int newMinZ = toZ - newViewDistance; ++ final int newMaxX = toX + newViewDistance; ++ final int newMaxZ = toZ + newViewDistance; ++ for (int currX = newMinX; currX <= newMaxX; ++currX) { ++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { ++ ++ // only add if we're outside the old view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ // x axis is width ++ // z axis is height ++ // right refers to the x axis of where we moved ++ // top refers to the z axis of where we moved ++ ++ // same view distance ++ ++ // used for relative positioning ++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise ++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise ++ ++ // The area excluded by overlapping the two view distance squares creates four rectangles: ++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section ++ // and on the right the "added" section. ++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually ++ // exclusive to the regions they surround. ++ ++ // 4 points of the rectangle ++ int maxX; // exclusive ++ int minX; // inclusive ++ int maxZ; // exclusive ++ int minZ; // inclusive ++ ++ if (dx != 0) { ++ // handle right addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX + (oldViewDistance * right) + right; // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle up addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = toX - (oldViewDistance * right); // inclusive ++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive ++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dx != 0) { ++ // handle left removal ++ ++ maxX = toX - (oldViewDistance * right); // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle down removal ++ ++ maxX = fromX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = toZ - (oldViewDistance * up); // exclusive ++ minZ = fromZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public final boolean remove() { ++ final int chunkX = this.lastChunkX; ++ final int chunkZ = this.lastChunkZ; ++ final int distance = this.distance; ++ if (chunkX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET; ++ ++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index e5d9c6f2cbe11c2ded6d8ad111fa6a8b2086dfba..c6d20bc2f0eab737338db6b88dacb63f0decb66c 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -1,5 +1,6 @@ + package io.papermc.paper.chunk.system.scheduling; + ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; + import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; + import co.aikar.timings.Timing; +@@ -493,6 +494,21 @@ public final class ChunkHolderManager { + } + } + ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ this.ticketLock.lock(); ++ try { ++ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier)) { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier); ++ return true; ++ } ++ return false; ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ + public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { + if (ticketLevel > MAX_TICKET_LEVEL) { + return; +@@ -900,6 +916,142 @@ public final class ChunkHolderManager { + } + } + ++ public enum TicketOperationType { ++ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE ++ } ++ ++ public static record TicketOperation ( ++ TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier, ++ TicketType ticketType2, int ticketLevel2, V identifier2 ++ ) { ++ ++ private TicketOperation(TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier) { ++ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); ++ } ++ ++ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addIfRemovedOp(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ ++ public static TicketOperation addAndRemove(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ } ++ ++ private final MultiThreadedQueue> delayedTicketUpdates = new MultiThreadedQueue<>(); ++ ++ // note: MUST hold ticket lock, otherwise operation ordering is lost ++ private boolean drainTicketUpdates() { ++ boolean ret = false; ++ ++ TicketOperation operation; ++ while ((operation = this.delayedTicketUpdates.poll()) != null) { ++ switch (operation.op) { ++ case ADD: { ++ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case REMOVE: { ++ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case ADD_IF_REMOVED: { ++ ret |= this.addIfRemovedTicket( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ case ADD_AND_REMOVE: { ++ ret = true; ++ this.addAndRemoveTickets( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public Boolean tryDrainTicketUpdates() { ++ final boolean acquired = this.ticketLock.tryLock(); ++ try { ++ if (!acquired) { ++ return null; ++ } ++ ++ return Boolean.valueOf(this.drainTicketUpdates()); ++ } finally { ++ if (acquired) { ++ this.ticketLock.unlock(); ++ } ++ } ++ } ++ ++ public void pushDelayedTicketUpdate(final TicketOperation operation) { ++ this.delayedTicketUpdates.add(operation); ++ } ++ ++ public void pushDelayedTicketUpdates(final Collection> operations) { ++ this.delayedTicketUpdates.addAll(operations); ++ } ++ ++ public Boolean tryProcessTicketUpdates() { ++ final boolean acquired = this.ticketLock.tryLock(); ++ try { ++ if (!acquired) { ++ return null; ++ } ++ ++ return Boolean.valueOf(this.processTicketUpdates(false, true, null)); ++ } finally { ++ if (acquired) { ++ this.ticketLock.unlock(); ++ } ++ } ++ } ++ + private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { + return Boolean.FALSE; + }); +@@ -948,6 +1100,8 @@ public final class ChunkHolderManager { + + this.ticketLock.lock(); + try { ++ this.drainTicketUpdates(); ++ + final boolean levelsUpdated = this.ticketLevelPropagator.propagateUpdates(); + if (levelsUpdated) { + // Unlike CB, ticket level updates cannot happen recursively. Thank god. +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 8d442c5a498ecf288a0cc0c54889c6e2fda849ce..9f5f0d8ddc8f480b48079c70e38c9c08eff403f6 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -287,4 +287,43 @@ public class GlobalConfiguration extends ConfigurationPart { + public boolean useDimensionTypeForCustomSpawners = false; + public boolean strictAdvancementDimensionCheck = false; + } ++ ++ public ChunkLoadingBasic chunkLoadingBasic; ++ ++ public class ChunkLoadingBasic extends ConfigurationPart { ++ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkSendRate = 75.0; ++ ++ @Comment( ++ "The maximum rate at which chunks will load for any individual player. " + ++ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" + ++ "chunk is already generated. Set to -1 to disable this limit." ++ ) ++ public double playerMaxChunkLoadRate = 100.0; ++ ++ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkGenerateRate = -1.0; ++ } ++ ++ public ChunkLoadingAdvanced chunkLoadingAdvanced; ++ ++ public class ChunkLoadingAdvanced extends ConfigurationPart { ++ @Comment( ++ "Set to true if the server will match the chunk send radius that clients have configured" + ++ "in their view distance settings if the client is less-than the server's send distance." ++ ) ++ public boolean autoConfigSendDistance = true; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkLoads = 0; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkGenerates = 0; ++ } + } +diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644 +--- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java ++++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +@@ -2,6 +2,8 @@ package io.papermc.paper.util; + + public final class IntervalledCounter { + ++ private static final int INITIAL_SIZE = 8; ++ + protected long[] times; + protected long[] counts; + protected final long interval; +@@ -11,8 +13,8 @@ public final class IntervalledCounter { + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { +- this.times = new long[8]; +- this.counts = new long[8]; ++ this.times = new long[INITIAL_SIZE]; ++ this.counts = new long[INITIAL_SIZE]; + this.interval = interval; + } + +@@ -67,13 +69,13 @@ public final class IntervalledCounter { + this.tail = nextTail; + } + +- public void updateAndAdd(final int count) { ++ public void updateAndAdd(final long count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } + +- public void updateAndAdd(final int count, final long currTime) { ++ public void updateAndAdd(final long count, final long currTime) { + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } +@@ -93,9 +95,13 @@ public final class IntervalledCounter { + this.tail = size; + + if (tail >= head) { ++ // sequentially ordered from [head, tail) + System.arraycopy(oldElements, head, newElements, 0, size); + System.arraycopy(oldCounts, head, newCounts, 0, size); + } else { ++ // ordered from [head, length) ++ // then followed by [0, tail) ++ + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + +@@ -106,10 +112,18 @@ public final class IntervalledCounter { + + // returns in units per second + public double getRate() { +- return this.size() / (this.interval * 1.0e-9); ++ return (double)this.sum / ((double)this.interval * 1.0E-9); ++ } ++ ++ public long getInterval() { ++ return this.interval; + } + +- public long size() { ++ public long getSum() { + return this.sum; + } ++ ++ public int totalDataPoints() { ++ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head)); ++ } + } +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index d1a59c2af0557a816c094983ec60097fb4de060c..6898c704e60d89d53c8ed114e5e12f73ed63605a 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -602,8 +602,8 @@ public final class MCUtil { + + worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld)); + worldData.addProperty("name", world.getWorld().getName()); +- worldData.addProperty("view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace chunk loader system +- worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system ++ worldData.addProperty("view-distance", world.getWorld().getViewDistance()); // Paper - replace chunk loader system ++ worldData.addProperty("tick-view-distance", world.getWorld().getSimulationDistance()); // Paper - replace chunk loader system + worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory); + worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16); + +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index bc46479fd0622a90fd98ac88f92b2840a22a2d04..0b9cb85c063f913ad9245bafb8587d2f06c0ac6e 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -128,6 +128,26 @@ public class ChunkHolder { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInChunkTickRange; + // Paper end - optimise anyPlayerCloseEnoughForSpawning + ++ // Paper start - replace player chunk loader ++ private final com.destroystokyo.paper.util.maplist.ReferenceList playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>(); ++ ++ public void addPlayer(ServerPlayer player) { ++ if (!this.playersSentChunkTo.add(player)) { ++ throw new IllegalStateException("Already sent chunk " + this.pos + " to player " + player); ++ } ++ } ++ ++ public void removePlayer(ServerPlayer player) { ++ if (!this.playersSentChunkTo.remove(player)) { ++ throw new IllegalStateException("Have not sent chunk " + this.pos + " to player " + player); ++ } ++ } ++ ++ public boolean hasChunkBeenSent() { ++ return this.playersSentChunkTo.size() != 0; ++ } ++ // Paper end - replace player chunk loader ++ + public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system + this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system + this.chunkToSaveHistory = null; +@@ -225,6 +245,11 @@ public class ChunkHolder { + // Paper - rewrite chunk system + + public void blockChanged(BlockPos pos) { ++ // Paper start - replace player chunk loader ++ if (this.playersSentChunkTo.size() == 0) { ++ return; ++ } ++ // Paper end - replace player chunk loader + LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { +@@ -251,7 +276,7 @@ public class ChunkHolder { + LevelChunk chunk = this.getSendingChunk(); + // Paper end - no-tick view distance + +- if (chunk != null) { ++ if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader + int j = this.lightEngine.getMinLightSection(); + int k = this.lightEngine.getMaxLightSection(); + +@@ -351,27 +376,32 @@ public class ChunkHolder { + + } + +- public void broadcast(Packet packet, boolean onlyOnWatchDistanceEdge) { +- // Paper start - per player view distance +- // there can be potential desync with player's last mapped section and the view distance map, so use the +- // view distance map here. +- com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); +- if (players == null) { +- return; +- } ++ // Paper start - rewrite player chunk loader ++ public List getPlayers(boolean onlyOnWatchDistanceEdge) { ++ List ret = new java.util.ArrayList<>(); + +- Object[] backingSet = players.getBackingSet(); +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- if (!(backingSet[i] instanceof ServerPlayer player)) { ++ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { ++ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i); ++ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { + continue; + } +- if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { ++ ret.add(player); ++ } ++ ++ return ret; ++ } ++ // Paper end - rewrite player chunk loader ++ ++ public void broadcast(Packet packet, boolean onlyOnWatchDistanceEdge) { ++ // Paper start - rewrite player chunk loader - modeled after the above ++ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { ++ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i); ++ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { + continue; + } + player.connection.send(packet); + } +- // Paper end - per player view distance ++ // Paper end - rewrite player chunk loader + } + + // Paper - rewrite chunk system +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 2212f9f48636357265d8e44aba415ea4f09f1fe7..870f4d6fae8c14502b4653f246a2df9e345ccca3 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -196,7 +196,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - use distance map to optimise tracker + + void addPlayerToDistanceMaps(ServerPlayer player) { +- this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader ++ this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated +@@ -218,7 +218,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { +- this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader ++ this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader + + // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + this.playerMobSpawnMap.remove(player); +@@ -241,7 +241,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated +- this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader ++ this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader + this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + // Paper start - per player mob spawning + if (this.playerMobDistanceMap != null) { +@@ -813,7 +813,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - replace player loader system + public void setTickViewDistance(int distance) { +- this.playerChunkManager.setTickDistance(distance); ++ this.level.playerChunkLoader.setTickDistance(distance); ++ } ++ ++ public void setSendViewDistance(int distance) { ++ this.level.playerChunkLoader.setSendDistance(distance); + } + // Paper end - replace player loader system + public void setViewDistance(int watchDistance) { +@@ -823,20 +827,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int k = this.viewDistance; + + this.viewDistance = j; +- this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system ++ this.level.playerChunkLoader.setLoadDistance(this.viewDistance); // Paper - replace player loader system + } + + } + + public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass + if (player.level == this.level) { ++ ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); // Paper - replace chunk loader system - move up + if (newWithinViewDistance && !oldWithinViewDistance) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); ++ // Paper - replace chunk loader system - move up + + if (playerchunk != null) { + LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system + + if (chunk != null) { ++ playerchunk.addPlayer(player); // Paper - replace chunk loader system + this.playerLoadedChunk(player, packet, chunk); + } + +@@ -845,6 +851,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + if (!newWithinViewDistance && oldWithinViewDistance) { ++ // Paper start - replace chunk loader system ++ if (playerchunk != null) { ++ playerchunk.removePlayer(player); ++ } ++ // Paper end - replace chunk loader system + player.untrackChunk(pos); + } + +@@ -1148,34 +1159,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper - replaced by PlayerChunkLoader + + this.updateMaps(player); // Paper - distance maps +- this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately + + } + + @Override + public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { + // Paper start - per player view distance +- // there can be potential desync with player's last mapped section and the view distance map, so use the +- // view distance map here. +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); +- if (players == null) { +- return java.util.Collections.emptyList(); +- } +- +- List ret = new java.util.ArrayList<>(players.size()); +- +- Object[] backingSet = players.getBackingSet(); +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- if (!(backingSet[i] instanceof ServerPlayer player)) { +- continue; +- } +- if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) { +- continue; +- } +- ret.add(player); ++ ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong()); ++ if (holder == null) { ++ return new java.util.ArrayList<>(); ++ } else { ++ return holder.getPlayers(onlyOnWatchDistanceEdge); + } +- +- return ret; + // Paper end - per player view distance + } + +@@ -1599,7 +1594,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + double vec3d_dx = player.getX() - this.entity.getX(); + double vec3d_dz = player.getZ() - this.entity.getZ(); + // Paper end - remove allocation of Vec3D here +- double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance ++ double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player) * 16); // Paper - per player view distance + double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper + double d2 = d0 * d0; + boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 52cba8f68d274cce106304aef1249a95474d3238..88fca8b160df6804f30ed2cf8cf1f645085434e2 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -184,17 +184,17 @@ public abstract class DistanceManager { + } + + protected void updatePlayerTickets(int viewDistance) { +- this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager ++ this.chunkMap.setViewDistance(viewDistance);// Paper - route to player chunk manager + } + + // Paper start + public int getSimulationDistance() { +- return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager ++ return this.chunkMap.level.playerChunkLoader.getAPITickDistance(); + } + // Paper end + + public void updateSimulationDistance(int simulationDistance) { +- this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager ++ this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager + } + + public int getNaturalSpawnChunkCount() { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index ca84eddbdb1e198b899750e5f6b3eafd25ce970f..736f37979c882e41e7571202df38eb6a2923fcb0 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -645,7 +645,7 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().popPush("chunks"); + if (tickChunks) { + this.level.timings.chunks.startTiming(); // Paper - timings +- this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes ++ this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes + this.tickChunks(); + this.level.timings.chunks.stopTiming(); // Paper - timings + } +@@ -1001,7 +1001,7 @@ public class ServerChunkCache extends ChunkSource { + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { +- ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); ++ ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader + if (ServerChunkCache.this.runDistanceManagerUpdates()) { + return true; + } +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 54c2b7fba83d6f06dba95b1bb5b487a02048d6e6..714637cdd9dcdbffa344b19e77944fb3c7541ff7 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -523,6 +523,48 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // Paper end - optimise get nearest players for entity AI + ++ public final io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader(this); ++ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances(-1, -1, -1)); ++ ++ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances getViewDistances() { ++ return this.viewDistances.get(); ++ } ++ ++ private void updateViewDistance(final java.util.function.Function update) { ++ for (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { ++ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { ++ return; ++ } ++ } ++ } ++ ++ public void setTickViewDistance(final int distance) { ++ if ((distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setTickViewDistance(distance); ++ }); ++ } ++ ++ public void setLoadViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setLoadViewDistance(distance); ++ }); ++ } ++ ++ public void setSendViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setSendViewDistance(distance); ++ }); ++ } ++ + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // Holder holder = worlddimension.type(); // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 7d6d3c8556033d289fdadc489e73fba478fce41a..869daafbc236b3ff63f878e5fe28427fde75afe5 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -269,6 +269,48 @@ public class ServerPlayer extends Player { + public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event + ++ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances(-1, -1, -1)); ++ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; ++ ++ public io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances getViewDistances() { ++ return this.viewDistances.get(); ++ } ++ ++ private void updateViewDistance(final java.util.function.Function update) { ++ for (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { ++ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { ++ return; ++ } ++ } ++ } ++ ++ public void setTickViewDistance(final int distance) { ++ if ((distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setTickViewDistance(distance); ++ }); ++ } ++ ++ public void setLoadViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setLoadViewDistance(distance); ++ }); ++ } ++ ++ public void setSendViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setSendViewDistance(distance); ++ }); ++ } ++ + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); + this.chatVisibility = ChatVisiblity.FULL; +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 4b754f6eae683248d7fe11d6d6cb168d5dd696a2..3c9d08c37a44a60bc70387d8d0dbd0a39ea98a26 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -270,7 +270,7 @@ public abstract class PlayerList { + boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); + + // Spigot - view distance +- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management ++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management + player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + playerconnection.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(worldserver1.enabledFeatures()))); + playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); +@@ -898,8 +898,8 @@ public abstract class PlayerList { + // CraftBukkit start + LevelData worlddata = worldserver1.getLevelData(); + entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), (byte) i, entityplayer1.getLastDeathLocation())); +- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management +- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management ++ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management ++ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management + entityplayer1.spawnIn(worldserver1); + entityplayer1.unsetRemoved(); + entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 3cbf801b2e5420c0e870f73788deb550e49ad54d..60003ff929f7ac6b34f9230c53ccbd54dc9e176b 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -627,7 +627,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); + // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance + // if copied from above +- } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(io.papermc.paper.util.MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management ++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management + ((ServerLevel)this).getChunkSource().blockChanged(blockposition); + // Paper end - per player view distance + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 28e4b302284f955a73e75d0f4276d55fb51826f5..e776eb8afef978938da084f9ae29d611181b43fe 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -184,43 +184,6 @@ public class LevelChunk extends ChunkAccess { + + protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { + +- // Paper start - no-tick view distance +- ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource(); +- net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap; +- // this code handles the addition of ticking tickets - the distance map handles the removal +- if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { +- if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system +- // now we're ready for entity ticking +- chunkProviderServer.mainThreadProcessor.execute(() -> { +- // double check that this condition still holds. +- if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system +- chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk +- chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update +- } +- }); +- } +- } +- +- // this code handles the chunk sending +- if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { +- // Paper start - replace old player chunk loading system +- if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) { +- // the post processing is expensive, so we don't want to run it unless we're actually near +- // a player. +- chunkProviderServer.mainThreadProcessor.execute(() -> { +- if (!LevelChunk.this.areNeighboursLoaded(1)) { +- return; +- } +- LevelChunk.this.postProcessGeneration(); +- if (!LevelChunk.this.areNeighboursLoaded(1)) { +- return; +- } +- chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); +- }); +- } +- // Paper end - replace old player chunk loading system +- } +- // Paper end - no-tick view distance + } + + public final boolean isAnyNeighborsLoaded() { +@@ -906,7 +869,6 @@ public class LevelChunk extends ChunkAccess { + // Paper - rewrite chunk system - move into separate callback + org.bukkit.Server server = this.level.getCraftServer(); + // Paper - rewrite chunk system - move into separate callback +- ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -1074,6 +1036,7 @@ public class LevelChunk extends ChunkAccess { + BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition); + + this.level.setBlock(blockposition, iblockdata1, 20); ++ if (iblockdata1 != iblockdata) this.level.chunkSource.blockChanged(blockposition); // Paper - replace player chunk loader - notify since we send before processing full updates + } + } + +@@ -1093,7 +1056,6 @@ public class LevelChunk extends ChunkAccess { + this.upgradeData.upgrade(this); + } finally { // Paper start - replace chunk loader system + this.isPostProcessingDone = true; +- this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z); + } + // Paper end - replace chunk loader system + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 4cb0307935aa63d44aac55c80ee50be074d7913c..d33476ffa49d7f6388bb227f8a57cf115a74698f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2257,12 +2257,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { + // Spigot start + @Override + public int getViewDistance() { +- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management ++ return this.getHandle().playerChunkLoader.getAPIViewDistance(); // Paper - replace player chunk loader + } + + @Override + public int getSimulationDistance() { +- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - replace old player chunk management ++ return this.getHandle().playerChunkLoader.getAPITickDistance(); // Paper - replace player chunk loader + } + // Spigot end + // Paper start - view distance api +@@ -2296,12 +2296,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getSendViewDistance() { +- return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); ++ return this.getHandle().playerChunkLoader.getAPISendViewDistance(); // Paper - replace player chunk loader + } + + @Override + public void setSendViewDistance(int viewDistance) { +- getHandle().getChunkSource().chunkMap.playerChunkManager.setSendDistance(viewDistance); ++ this.getHandle().chunkSource.chunkMap.setSendViewDistance(viewDistance); // Paper - replace player chunk loader + } + // Paper end - view distance api + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 7c43de6ad6bd7259c6bcb2a55e312e8abfcf546b..0351eb67bac6ce257f820af60aa3bba9f45da687 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -188,44 +188,22 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + // Paper start - implement view distances + @Override + public int getViewDistance() { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); +- } +- return data.getTargetNoTickViewDistance(); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPIViewDistance(this); + } + + @Override + public void setViewDistance(int viewDistance) { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- throw new IllegalStateException("Player is not attached to world"); +- } +- +- data.setTargetNoTickViewDistance(viewDistance); ++ this.getHandle().setLoadViewDistance(viewDistance < 0 ? viewDistance : viewDistance + 1); + } + + @Override + public int getSimulationDistance() { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- return chunkMap.playerChunkManager.getTargetTickViewDistance(); +- } +- return data.getTargetTickViewDistance(); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPITickViewDistance(this); + } + + @Override + public void setSimulationDistance(int simulationDistance) { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- throw new IllegalStateException("Player is not attached to world"); +- } +- +- data.setTargetTickViewDistance(simulationDistance); ++ this.getHandle().setTickViewDistance(simulationDistance); + } + + @Override +@@ -240,23 +218,12 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public int getSendViewDistance() { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- return chunkMap.playerChunkManager.getTargetSendDistance(); +- } +- return data.getTargetSendViewDistance(); ++ return io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader.getAPISendViewDistance(this); + } + + @Override + public void setSendViewDistance(int viewDistance) { +- net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; +- io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); +- if (data == null) { +- throw new IllegalStateException("Player is not attached to world"); +- } +- +- data.setTargetSendViewDistance(viewDistance); ++ this.getHandle().setSendViewDistance(viewDistance); + } + // Paper end - implement view distances + diff --git a/patches/server/0003-Make-ChunkStatus.EMPTY-not-rely-on-the-main-thread-f.patch b/patches/server/0003-Make-ChunkStatus.EMPTY-not-rely-on-the-main-thread-f.patch new file mode 100644 index 0000000..0238da0 --- /dev/null +++ b/patches/server/0003-Make-ChunkStatus.EMPTY-not-rely-on-the-main-thread-f.patch @@ -0,0 +1,395 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 16 Feb 2023 16:50:05 -0800 +Subject: [PATCH] Make ChunkStatus.EMPTY not rely on the main thread for + completion + +In order to do this, we need to push the POI consistency checks +to a later status. Since FULL is the only other status that +uses the main thread, it can go there. + +The consistency checks are only really for when a desync occurs, +and so that delaying the check only matters when the chunk data +has desync'd. As long as the desync is sorted before the +chunk is full loaded (i.e before setBlock can occur on +a chunk), it should not matter. + +This change is primarily due to behavioural changes +in the chunk task queue brought by region threading - +which is to split the queue into separate regions. As such, +it is required that in order for the sync load to complete +that the region owning the chunk drain and execute the task +while ticking. However, that is not always possible in +region threading. Thus, removing the main thread reliance allows +the chunk to progress without requiring a tick thread. +Specifically, this allows far sync loads (outside of a specific +regions bounds) to occur without issue - namely with structure +searching. + +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +index fb42d776f15f735fb59e972e00e2b512c23a8387..300700477ee34bc22b31315825c0e40f61070cd5 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +@@ -2,6 +2,8 @@ package io.papermc.paper.chunk.system.scheduling; + + import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.poi.PoiChunk; + import net.minecraft.server.level.ChunkMap; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.world.level.chunk.ChunkAccess; +@@ -9,10 +11,13 @@ import net.minecraft.world.level.chunk.ChunkStatus; + import net.minecraft.world.level.chunk.ImposterProtoChunk; + import net.minecraft.world.level.chunk.LevelChunk; + import net.minecraft.world.level.chunk.ProtoChunk; ++import org.slf4j.Logger; + import java.lang.invoke.VarHandle; + + public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { + ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ + protected final NewChunkHolder chunkHolder; + protected final ChunkAccess fromChunk; + protected final PrioritisedExecutor.PrioritisedTask convertToFullTask; +@@ -35,6 +40,15 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl + // See Vanilla protoChunkToFullChunk for what this function should be doing + final LevelChunk chunk; + try { ++ // moved from the load from nbt stage into here ++ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); ++ if (poiChunk == null) { ++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); ++ } else { ++ poiChunk.load(); ++ this.world.getPoiManager().checkConsistency(this.fromChunk); ++ } ++ + if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { + chunk = wrappedFull.getWrapped(); + } else { +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +index 3df793f7e6bb67f40e7387a72fdafb912a7b1373..31657c387156f789d5c04ad3413d049bc32f1359 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +@@ -25,6 +25,7 @@ import org.slf4j.Logger; + import java.lang.invoke.VarHandle; + import java.util.Map; + import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; + import java.util.function.Consumer; + + public final class ChunkLoadTask extends ChunkProgressionTask { +@@ -34,9 +35,11 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + private final NewChunkHolder chunkHolder; + private final ChunkDataLoadTask loadTask; + +- private boolean cancelled; ++ private volatile boolean cancelled; + private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ private GenericDataLoadTask.TaskResult loadResult; ++ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data + + protected ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, + final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { +@@ -44,10 +47,18 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + this.chunkHolder = chunkHolder; + this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); + this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { +- ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); ++ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement ++ ChunkLoadTask.this.tryCompleteLoad(); + }); + } + ++ private void tryCompleteLoad() { ++ if (this.taskCountToComplete.decrementAndGet() == 0) { ++ final GenericDataLoadTask.TaskResult result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement ++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); ++ } ++ } ++ + @Override + public ChunkStatus getTargetStatus() { + return ChunkStatus.EMPTY; +@@ -65,11 +76,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; + final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; + +- final AtomicInteger count = new AtomicInteger(); + final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { +- if (count.decrementAndGet() == 0) { +- ChunkLoadTask.this.loadTask.schedule(false); +- } ++ ChunkLoadTask.this.tryCompleteLoad(); + }; + + // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because +@@ -85,16 +93,16 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + if (!this.chunkHolder.isEntityChunkNBTLoaded()) { + entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); +- count.setPlain(count.getPlain() + 1); + } else { + entityLoadTask = null; ++ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled + } + + if (!this.chunkHolder.isPoiChunkLoaded()) { + poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); +- count.setPlain(count.getPlain() + 1); + } else { + poiLoadTask = null; ++ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled + } + + this.entityLoadTask = entityLoadTask; +@@ -107,14 +115,11 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + entityLoadTask.schedule(); + } + +- if (poiLoadTask != null) { ++ if (poiLoadTask != null) { + poiLoadTask.schedule(); + } + +- if (entityLoadTask == null && poiLoadTask == null) { +- // no need to wait on those, we can schedule now +- this.loadTask.schedule(false); +- } ++ this.loadTask.schedule(false); + } + + @Override +@@ -129,15 +134,20 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + + /* + Note: The entityLoadTask/poiLoadTask do not complete when cancelled, +- but this is fine because if they are successfully cancelled then +- we will successfully cancel the load task, which will complete when cancelled ++ so we need to manually try to complete in those cases ++ It is also important to note that we set the cancelled field first, just in case ++ the chunk load task attempts to complete with a non-null value + */ + + if (this.entityLoadTask != null) { +- this.entityLoadTask.cancel(); ++ if (this.entityLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } + } + if (this.poiLoadTask != null) { +- this.poiLoadTask.cancel(); ++ if (this.poiLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } + } + this.loadTask.cancel(); + } +@@ -249,7 +259,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + } + } + +- public final class ChunkDataLoadTask extends CallbackDataLoadTask { ++ public static final class ChunkDataLoadTask extends CallbackDataLoadTask { + protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final PrioritisedExecutor.Priority priority) { + super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); +@@ -262,7 +272,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + + @Override + protected boolean hasOnMain() { +- return true; ++ return false; + } + + @Override +@@ -272,35 +282,30 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + + @Override + protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { +- return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); ++ throw new UnsupportedOperationException(); + } + + @Override +- protected TaskResult completeOnMainOffMain(final ChunkSerializer.InProgressChunkHolder data, final Throwable throwable) { +- if (data != null) { +- return null; +- } +- +- final PoiChunk poiChunk = ChunkLoadTask.this.chunkHolder.getPoiChunk(); +- if (poiChunk == null) { +- LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); +- } else if (!poiChunk.isLoaded()) { +- // need to call poiChunk.load() on main +- return null; +- } ++ protected TaskResult completeOnMainOffMain(final ChunkAccess data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } + +- return new TaskResult<>(this.getEmptyChunk(), null); ++ private ProtoChunk getEmptyChunk() { ++ return new ProtoChunk( ++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, ++ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null ++ ); + } + + @Override +- protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { ++ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { + if (throwable != null) { + LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); +- return new TaskResult<>(null, null); ++ return new TaskResult<>(this.getEmptyChunk(), null); + } + + if (data == null) { +- return new TaskResult<>(null, null); ++ return new TaskResult<>(this.getEmptyChunk(), null); + } + + // need to convert data, and then deserialize it +@@ -319,53 +324,18 @@ public final class ChunkLoadTask extends ChunkProgressionTask { + this.world, chunkMap.getPoiManager(), chunkPos, converted, true + ); + +- return new TaskResult<>(chunkHolder, null); ++ return new TaskResult<>(chunkHolder.protoChunk, null); + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); +- return new TaskResult<>(null, thr2); ++ return new TaskResult<>(this.getEmptyChunk(), thr2); + } + } + +- private ProtoChunk getEmptyChunk() { +- return new ProtoChunk( +- new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, +- this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null +- ); +- } +- + @Override +- protected TaskResult runOnMain(final ChunkSerializer.InProgressChunkHolder data, final Throwable throwable) { +- final PoiChunk poiChunk = ChunkLoadTask.this.chunkHolder.getPoiChunk(); +- if (poiChunk == null) { +- LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); +- } else { +- poiChunk.load(); +- } +- +- if (data == null || data.protoChunk == null) { +- // throwable could be non-null, but the off-main task will print its exceptions - so we don't need to care, +- // it's handled already +- +- return new TaskResult<>(this.getEmptyChunk(), null); +- } +- +- // have tasks to run (at this point, it's just the POI consistency checking) +- try { +- if (data.tasks != null) { +- for (int i = 0, len = data.tasks.size(); i < len; ++i) { +- data.tasks.poll().run(); +- } +- } +- +- return new TaskResult<>(data.protoChunk, null); +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable thr2) { +- LOGGER.error("Failed to parse main tasks for task " + this.toString() + ", chunk data will be lost", thr2); +- return new TaskResult<>(this.getEmptyChunk(), null); +- } ++ protected TaskResult runOnMain(final ChunkAccess data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 8950b220b9a3512cd4667beb7bdec0e82e07edc6..9be85eb0abec02bc0e0eded71c34ab1c565c63e7 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -328,6 +328,12 @@ public class PoiManager extends SectionStorage { + } + } + } ++ ++ public void checkConsistency(net.minecraft.world.level.chunk.ChunkAccess chunk) { ++ for (LevelChunkSection section : chunk.getSections()) { ++ this.checkConsistencyWithBlocks(chunk.getPos(), section); ++ } ++ } + // Paper end - rewrite chunk system + + public void checkConsistencyWithBlocks(ChunkPos chunkPos, LevelChunkSection chunkSection) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index d4c4d37bcef14e392739d9aae9e20b7d69b05c12..256642f2e2aa66f7e8c00cae91a75060a8817c9c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -122,13 +122,11 @@ public class ChunkSerializer { + public static final class InProgressChunkHolder { + + public final ProtoChunk protoChunk; +- public final java.util.ArrayDeque tasks; + + public CompoundTag poiData; + +- public InProgressChunkHolder(final ProtoChunk protoChunk, final java.util.ArrayDeque tasks) { ++ public InProgressChunkHolder(final ProtoChunk protoChunk) { + this.protoChunk = protoChunk; +- this.tasks = tasks; + } + } + // Paper end +@@ -136,7 +134,6 @@ public class ChunkSerializer { + public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { + // Paper start - add variant for async calls + InProgressChunkHolder holder = loadChunk(world, poiStorage, chunkPos, nbt, true); +- holder.tasks.forEach(Runnable::run); + return holder.protoChunk; + } + +@@ -145,7 +142,6 @@ public class ChunkSerializer { + private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion"); + // Paper end + public static InProgressChunkHolder loadChunk(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt, boolean distinguish) { +- java.util.ArrayDeque tasksToExecuteOnMain = new java.util.ArrayDeque<>(); + // Paper end + // Paper start - Do NOT attempt to load chunks saved with newer versions + if (nbt.contains("DataVersion", 99)) { +@@ -223,9 +219,7 @@ public class ChunkSerializer { + LevelChunkSection chunksection = new LevelChunkSection(b0, datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write + + achunksection[k] = chunksection; +- tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main +- poiStorage.checkConsistencyWithBlocks(chunkPos, chunksection); +- }); // Paper - delay this task since we're executing off-main ++ // Paper - rewrite chunk system - moved to final load stage + } + + boolean flag3 = nbttagcompound1.contains("BlockLight", 7); +@@ -403,7 +397,7 @@ public class ChunkSerializer { + } + + if (chunkstatus_type == ChunkStatus.ChunkType.LEVELCHUNK) { +- return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false), tasksToExecuteOnMain); // Paper - Async chunk loading ++ return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false)); // Paper - Async chunk loading + } else { + ProtoChunk protochunk1 = (ProtoChunk) object1; + +@@ -446,7 +440,7 @@ public class ChunkSerializer { + protochunk1.setCarvingMask(worldgenstage_features, new CarvingMask(nbttagcompound4.getLongArray(s1), ((ChunkAccess) object1).getMinBuildHeight())); + } + +- return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading ++ return new InProgressChunkHolder(protochunk1); // Paper - Async chunk loading + } + } + diff --git a/patches/server/0004-Threaded-Regions.patch b/patches/server/0004-Threaded-Regions.patch new file mode 100644 index 0000000..6d27f0f --- /dev/null +++ b/patches/server/0004-Threaded-Regions.patch @@ -0,0 +1,20522 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 2 Oct 2022 21:28:53 -0700 +Subject: [PATCH] Threaded Regions + +Connection thread-safety fixes + +- send packet +- pending addition + +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java +index f4415f782b32fed25da98e44b172f717c4d46e34..ba7c24b3627a1827721d2462add15fdd4adbed90 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java +@@ -392,6 +392,24 @@ public class MultiThreadedQueue implements Queue { + } + } + ++ /** ++ * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}. ++ */ ++ public boolean isAddBlocked() { ++ for (LinkedNode tail = this.getTailOpaque();;) { ++ LinkedNode next = tail.getNextVolatile(); ++ if (next == null) { ++ return false; ++ } ++ ++ if (next == tail) { ++ return true; ++ } ++ ++ tail = next; ++ } ++ } ++ + /** + * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no + * head is removed. +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ImproveReentrantLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ImproveReentrantLock.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9df9881396f4a69b51acaae562b12b8ce0a48443 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ImproveReentrantLock.java +@@ -0,0 +1,139 @@ ++package ca.spottedleaf.concurrentutil.lock; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.locks.AbstractQueuedSynchronizer; ++import java.util.concurrent.locks.Condition; ++import java.util.concurrent.locks.Lock; ++ ++/** ++ * Implementation of {@link Lock} that should outperform {@link java.util.concurrent.locks.ReentrantLock}. ++ * The lock is considered a non-fair lock, as specified by {@link java.util.concurrent.locks.ReentrantLock}, ++ * and additionally does not support the creation of Conditions. ++ * ++ *

++ * Specifically, this implementation is careful to avoid synchronisation penalties when multi-acquiring and ++ * multi-releasing locks from the same thread, and additionally avoids unnecessary synchronisation penalties ++ * when releasing the lock. ++ *

++ */ ++public class ImproveReentrantLock implements Lock { ++ ++ private final InternalLock lock = new InternalLock(); ++ ++ private static final class InternalLock extends AbstractQueuedSynchronizer { ++ ++ private volatile Thread owner; ++ private static final VarHandle OWNER_HANDLE = ConcurrentUtil.getVarHandle(InternalLock.class, "owner", Thread.class); ++ private int count; ++ ++ private Thread getOwnerPlain() { ++ return (Thread)OWNER_HANDLE.get(this); ++ } ++ ++ private Thread getOwnerVolatile() { ++ return (Thread)OWNER_HANDLE.getVolatile(this); ++ } ++ ++ private void setOwnerRelease(final Thread to) { ++ OWNER_HANDLE.setRelease(this, to); ++ } ++ ++ private void setOwnerVolatile(final Thread to) { ++ OWNER_HANDLE.setVolatile(this, to); ++ } ++ ++ private Thread compareAndExchangeOwnerVolatile(final Thread expect, final Thread update) { ++ return (Thread)OWNER_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ @Override ++ protected final boolean tryAcquire(int acquires) { ++ final Thread current = Thread.currentThread(); ++ final Thread owner = this.getOwnerVolatile(); ++ ++ // When trying to blind acquire the lock, using just compare and exchange is faster ++ // than reading the owner field first - but comes at the cost of performing the compare and exchange ++ // even if the current thread owns the lock ++ if ((owner == null && null == this.compareAndExchangeOwnerVolatile(null, current)) || owner == current) { ++ this.count += acquires; ++ return true; ++ } ++ ++ return false; ++ } ++ ++ @Override ++ protected final boolean tryRelease(int releases) { ++ if (this.getOwnerPlain() == Thread.currentThread()) { ++ final int newCount = this.count -= releases; ++ if (newCount == 0) { ++ // When the caller, which is release(), attempts to signal the next node, it will use volatile ++ // to retrieve the node and status. ++ // Let's say that we have written this field null as release, and then checked for a next node ++ // using volatile and then determined there are no waiters. ++ // While a call to tryAcquire() can fail for another thread since the write may not ++ // publish yet, once the thread adds itself to the waiters list it will synchronise with ++ // the write to the field, since the volatile write to put the thread on the waiter list ++ // will synchronise with the volatile read we did earlier to check for any ++ // waiters. ++ this.setOwnerRelease(null); ++ return true; ++ } ++ return false; ++ } ++ throw new IllegalMonitorStateException(); ++ } ++ } ++ ++ /** ++ * Returns the thread that owns the lock, or returns {@code null} if there is no such thread. ++ */ ++ public Thread getLockOwner() { ++ return this.lock.getOwnerVolatile(); ++ } ++ ++ /** ++ * Returns whether the current thread owns the lock. ++ */ ++ public boolean isHeldByCurrentThread() { ++ return this.lock.getOwnerPlain() == Thread.currentThread(); ++ } ++ ++ @Override ++ public void lock() { ++ this.lock.acquire(1); ++ } ++ ++ @Override ++ public void lockInterruptibly() throws InterruptedException { ++ if (Thread.interrupted()) { ++ throw new InterruptedException(); ++ } ++ this.lock.acquireInterruptibly(1); ++ } ++ ++ @Override ++ public boolean tryLock() { ++ return this.lock.tryAcquire(1); ++ } ++ ++ @Override ++ public boolean tryLock(final long time, final TimeUnit unit) throws InterruptedException { ++ if (Thread.interrupted()) { ++ throw new InterruptedException(); ++ } ++ return this.lock.tryAcquire(1) || this.lock.tryAcquireNanos(1, unit.toNanos(time)); ++ } ++ ++ @Override ++ public void unlock() { ++ this.lock.release(1); ++ } ++ ++ @Override ++ public Condition newCondition() { ++ throw new UnsupportedOperationException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java +new file mode 100644 +index 0000000000000000000000000000000000000000..793a7326141b7d83395585b3d32b0a7e8a6238a7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java +@@ -0,0 +1,303 @@ ++package ca.spottedleaf.concurrentutil.lock; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.locks.Condition; ++import java.util.concurrent.locks.Lock; ++import java.util.concurrent.locks.LockSupport; ++ ++// ReentrantBiasedLock ++public final class RBLock implements Lock { ++ ++ private volatile LockWaiter owner; ++ private static final VarHandle OWNER_HANDLE = ConcurrentUtil.getVarHandle(RBLock.class, "owner", LockWaiter.class); ++ ++ private volatile LockWaiter tail; ++ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(RBLock.class, "tail", LockWaiter.class); ++ ++ public RBLock() { ++ // we can have the initial state as if it was locked by this thread, then unlocked ++ final LockWaiter dummy = new LockWaiter(null, LockWaiter.STATE_BIASED, null); ++ this.setOwnerPlain(dummy); ++ // release ensures correct publishing ++ this.setTailRelease(dummy); ++ } ++ ++ private LockWaiter getOwnerVolatile() { ++ return (LockWaiter)OWNER_HANDLE.getVolatile(this); ++ } ++ ++ private void setOwnerPlain(final LockWaiter value) { ++ OWNER_HANDLE.set(this, value); ++ } ++ ++ private void setOwnerRelease(final LockWaiter value) { ++ OWNER_HANDLE.setRelease(this, value); ++ } ++ ++ ++ ++ private void setTailOpaque(final LockWaiter newTail) { ++ TAIL_HANDLE.setOpaque(this, newTail); ++ } ++ ++ private void setTailRelease(final LockWaiter newTail) { ++ TAIL_HANDLE.setRelease(this, newTail); ++ } ++ ++ private LockWaiter getTailOpaque() { ++ return (LockWaiter)TAIL_HANDLE.getOpaque(this); ++ } ++ ++ ++ private void appendWaiter(final LockWaiter waiter) { ++ // Similar to MultiThreadedQueue#appendList ++ int failures = 0; ++ ++ for (LockWaiter currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final LockWaiter next = curr.getNextVolatile(); ++ ++ for (int i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ ++ if (next == null) { ++ final LockWaiter compared = curr.compareAndExchangeNextVolatile(null, waiter); ++ ++ if (compared == null) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* CAS to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(waiter); ++ } ++ return; ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ // required that expected is already appended to the wait chain ++ private boolean tryAcquireBiased(final LockWaiter expected) { ++ final LockWaiter owner = this.getOwnerVolatile(); ++ if (owner.getNextVolatile() == expected && owner.getStateVolatile() == LockWaiter.STATE_BIASED) { ++ this.setOwnerRelease(expected); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void lock() { ++ final Thread currThread = Thread.currentThread(); ++ final LockWaiter owner = this.getOwnerVolatile(); ++ ++ // try to fast acquire ++ ++ final LockWaiter acquireObj; ++ boolean needAppend = true; ++ ++ if (owner.getNextVolatile() != null) { ++ // unlikely we are able to fast acquire ++ acquireObj = new LockWaiter(currThread, 1, null); ++ } else { ++ // may be able to fast acquire the lock ++ if (owner.owner == currThread) { ++ final int oldState = owner.incrementState(); ++ if (oldState == LockWaiter.STATE_BIASED) { ++ // in this case, we may not have the lock. ++ final LockWaiter next = owner.getNextVolatile(); ++ if (next == null) { ++ // we win the lock ++ return; ++ } else { ++ // we have incremented the state, which means any tryAcquireBiased() will fail. ++ // The next waiter may be waiting for us, so we need to re-set our state and then ++ // try to push the lock to them. ++ // We cannot simply claim ownership of the lock, since we don't know if the next waiter saw ++ // the biased state ++ owner.setStateRelease(LockWaiter.STATE_BIASED); ++ LockSupport.unpark(next.owner); ++ ++ acquireObj = new LockWaiter(currThread, 1, null); ++ // fall through to slower lock logic ++ } ++ } else { ++ // we already have the lock ++ return; ++ } ++ } else { ++ acquireObj = new LockWaiter(currThread, 1, null); ++ if (owner.getStateVolatile() == LockWaiter.STATE_BIASED) { ++ // we may be able to quickly acquire the lock ++ if (owner.getNextVolatile() == null && null == owner.compareAndExchangeNextVolatile(null, acquireObj)) { ++ if (owner.getStateVolatile() == LockWaiter.STATE_BIASED) { ++ this.setOwnerRelease(acquireObj); ++ return; ++ } else { ++ needAppend = false; ++ // we failed to acquire, but we can block instead - we did CAS to the next immediate owner ++ } ++ } ++ } // else: fall through to append and wait code ++ } ++ } ++ ++ if (needAppend) { ++ this.appendWaiter(acquireObj); // append to end of waiters ++ } ++ ++ // failed to fast acquire, so now we may need to block ++ final int spinAttempts = 10; ++ for (int i = 0; i < spinAttempts; ++i) { ++ for (int k = 0; k <= i; ++i) { ++ Thread.onSpinWait(); ++ } ++ if (this.tryAcquireBiased(acquireObj)) { ++ // acquired ++ return; ++ } ++ } ++ ++ // slow acquire ++ while (!this.tryAcquireBiased(acquireObj)) { ++ LockSupport.park(this); ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ * @throws IllegalMonitorStateException If the current thread does not own the lock. ++ */ ++ @Override ++ public void unlock() { ++ final LockWaiter owner = this.getOwnerVolatile(); ++ ++ final int oldState; ++ if (owner.owner != Thread.currentThread() || (oldState = owner.getStatePlain()) <= 0) { ++ throw new IllegalMonitorStateException(); ++ } ++ ++ owner.setStateRelease(oldState - 1); ++ ++ if (oldState != 1) { ++ return; ++ } ++ ++ final LockWaiter next = owner.getNextVolatile(); ++ ++ if (next == null) { ++ // we can leave the lock in biased state, which will save a CAS ++ return; ++ } ++ ++ // we have TWO cases: ++ // waiter saw the lock in biased state ++ // waiter did not see the lock in biased state ++ // the problem is that if the waiter saw the lock in the biased state, then it now owns the lock. but if it did not, ++ // then we still own the lock. ++ ++ // However, by unparking always, the waiter will try to acquire the biased lock from us. ++ LockSupport.unpark(next.owner); ++ } ++ ++ @Override ++ public void lockInterruptibly() throws InterruptedException { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean tryLock() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public Condition newCondition() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ static final class LockWaiter { ++ ++ static final int STATE_BIASED = 0; ++ ++ private volatile LockWaiter next; ++ private volatile int state; ++ private Thread owner; ++ ++ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LockWaiter.class, "next", LockWaiter.class); ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LockWaiter.class, "state", int.class); ++ ++ ++ private LockWaiter compareAndExchangeNextVolatile(final LockWaiter expect, final LockWaiter update) { ++ return (LockWaiter)NEXT_HANDLE.compareAndExchange((LockWaiter)this, expect, update); ++ } ++ ++ private void setNextPlain(final LockWaiter next) { ++ NEXT_HANDLE.set((LockWaiter)this, next); ++ } ++ ++ private LockWaiter getNextOpaque() { ++ return (LockWaiter)NEXT_HANDLE.getOpaque((LockWaiter)this); ++ } ++ ++ private LockWaiter getNextVolatile() { ++ return (LockWaiter)NEXT_HANDLE.getVolatile((LockWaiter)this); ++ } ++ ++ ++ ++ private int getStatePlain() { ++ return (int)STATE_HANDLE.get((LockWaiter)this); ++ } ++ ++ private int getStateVolatile() { ++ return (int)STATE_HANDLE.getVolatile((LockWaiter)this); ++ } ++ ++ private void setStatePlain(final int value) { ++ STATE_HANDLE.set((LockWaiter)this, value); ++ } ++ ++ private void setStateRelease(final int value) { ++ STATE_HANDLE.setRelease((LockWaiter)this, value); ++ } ++ ++ public LockWaiter(final Thread owner, final int initialState, final LockWaiter next) { ++ this.owner = owner; ++ this.setStatePlain(initialState); ++ this.setNextPlain(next); ++ } ++ ++ public int incrementState() { ++ final int old = this.getStatePlain(); ++ // Technically, we DO NOT need release for old != BIASED. But we care about optimising only for x86, ++ // which is a simple MOV for everything but volatile. ++ this.setStateRelease(old + 1); ++ return old; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7869cc177c95e26dd9e1d3db5b50e996956edb24 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java +@@ -0,0 +1,664 @@ ++package ca.spottedleaf.concurrentutil.map; ++ ++import ca.spottedleaf.concurrentutil.util.ArrayUtil; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import io.papermc.paper.util.IntegerUtil; ++import java.lang.invoke.VarHandle; ++import java.util.Arrays; ++import java.util.function.Consumer; ++import java.util.function.IntConsumer; ++ ++public class SWMRInt2IntHashTable { ++ ++ protected int size; ++ ++ protected TableEntry[] table; ++ ++ protected final float loadFactor; ++ ++ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "size", int.class); ++ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "table", TableEntry[].class); ++ ++ /* size */ ++ ++ protected final int getSizePlain() { ++ return (int)SIZE_HANDLE.get(this); ++ } ++ ++ protected final int getSizeOpaque() { ++ return (int)SIZE_HANDLE.getOpaque(this); ++ } ++ ++ protected final int getSizeAcquire() { ++ return (int)SIZE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setSizePlain(final int value) { ++ SIZE_HANDLE.set(this, value); ++ } ++ ++ protected final void setSizeOpaque(final int value) { ++ SIZE_HANDLE.setOpaque(this, value); ++ } ++ ++ protected final void setSizeRelease(final int value) { ++ SIZE_HANDLE.setRelease(this, value); ++ } ++ ++ /* table */ ++ ++ protected final TableEntry[] getTablePlain() { ++ //noinspection unchecked ++ return (TableEntry[])TABLE_HANDLE.get(this); ++ } ++ ++ protected final TableEntry[] getTableAcquire() { ++ //noinspection unchecked ++ return (TableEntry[])TABLE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setTablePlain(final TableEntry[] table) { ++ TABLE_HANDLE.set(this, table); ++ } ++ ++ protected final void setTableRelease(final TableEntry[] table) { ++ TABLE_HANDLE.setRelease(this, table); ++ } ++ ++ protected static final int DEFAULT_CAPACITY = 16; ++ protected static final float DEFAULT_LOAD_FACTOR = 0.75f; ++ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. ++ */ ++ public SWMRInt2IntHashTable() { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor of {@code 0.75f}. ++ * @param capacity specified initial capacity, > 0 ++ */ ++ public SWMRInt2IntHashTable(final int capacity) { ++ this(capacity, DEFAULT_LOAD_FACTOR); ++ } ++ ++ /** ++ * Constructs this map with the specified capacity and load factor. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 && finite ++ */ ++ public SWMRInt2IntHashTable(final int capacity, final float loadFactor) { ++ final int tableSize = getCapacityFor(capacity); ++ ++ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { ++ throw new IllegalArgumentException("Invalid load factor: " + loadFactor); ++ } ++ ++ //noinspection unchecked ++ final TableEntry[] table = new TableEntry[tableSize]; ++ this.setTablePlain(table); ++ ++ if (tableSize == MAXIMUM_CAPACITY) { ++ this.threshold = -1; ++ } else { ++ this.threshold = getTargetCapacity(tableSize, loadFactor); ++ } ++ ++ this.loadFactor = loadFactor; ++ } ++ ++ /** ++ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param other The specified map. ++ */ ++ public SWMRInt2IntHashTable(final SWMRInt2IntHashTable other) { ++ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with a load factor of {@code 0.75f}. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param other The specified map. ++ */ ++ public SWMRInt2IntHashTable(final int capacity, final SWMRInt2IntHashTable other) { ++ this(capacity, DEFAULT_LOAD_FACTOR, other); ++ } ++ ++ /** ++ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and ++ * with the specified load factor. ++ * All of the specified map's entries are copied into this map. ++ * @param capacity specified capacity, > 0 ++ * @param loadFactor specified load factor, > 0 && finite ++ * @param other The specified map. ++ */ ++ public SWMRInt2IntHashTable(final int capacity, final float loadFactor, final SWMRInt2IntHashTable other) { ++ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); ++ this.putAll(other); ++ } ++ ++ public final float getLoadFactor() { ++ return this.loadFactor; ++ } ++ ++ protected static int getCapacityFor(final int capacity) { ++ if (capacity <= 0) { ++ throw new IllegalArgumentException("Invalid capacity: " + capacity); ++ } ++ if (capacity >= MAXIMUM_CAPACITY) { ++ return MAXIMUM_CAPACITY; ++ } ++ return IntegerUtil.roundCeilLog2(capacity); ++ } ++ ++ /** Callers must still use acquire when reading the value of the entry. */ ++ protected final TableEntry getEntryForOpaque(final int key) { ++ final int hash = SWMRInt2IntHashTable.getHash(key); ++ final TableEntry[] table = this.getTableAcquire(); ++ ++ for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { ++ if (key == curr.key) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ protected final TableEntry getEntryForPlain(final int key) { ++ final int hash = SWMRInt2IntHashTable.getHash(key); ++ final TableEntry[] table = this.getTablePlain(); ++ ++ for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { ++ if (key == curr.key) { ++ return curr; ++ } ++ } ++ ++ return null; ++ } ++ ++ /* MT-Safe */ ++ ++ /** must be deterministic given a key */ ++ protected static int getHash(final int key) { ++ return it.unimi.dsi.fastutil.HashCommon.mix(key); ++ } ++ ++ // rets -1 if capacity*loadFactor is too large ++ protected static int getTargetCapacity(final int capacity, final float loadFactor) { ++ final double ret = (double)capacity * (double)loadFactor; ++ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { ++ return -1; ++ } ++ ++ return (int)ret; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ /* Make no attempt to deal with concurrent modifications */ ++ if (!(obj instanceof SWMRInt2IntHashTable)) { ++ return false; ++ } ++ final SWMRInt2IntHashTable other = (SWMRInt2IntHashTable)obj; ++ ++ if (this.size() != other.size()) { ++ return false; ++ } ++ ++ final TableEntry[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final int value = curr.getValueAcquire(); ++ ++ final int otherValue = other.get(curr.key); ++ if (value != otherValue) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ /* Make no attempt to deal with concurrent modifications */ ++ int hash = 0; ++ final TableEntry[] table = this.getTableAcquire(); ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ hash += curr.hashCode(); ++ } ++ } ++ ++ return hash; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public String toString() { ++ final StringBuilder builder = new StringBuilder(64); ++ builder.append("SingleWriterMultiReaderHashMap:{"); ++ ++ this.forEach((final int key, final int value) -> { ++ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); ++ }); ++ ++ return builder.append('}').toString(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public SWMRInt2IntHashTable clone() { ++ return new SWMRInt2IntHashTable(this.getTableAcquire().length, this.loadFactor, this); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void forEach(final Consumer action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr); ++ } ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface BiIntIntConsumer { ++ public void accept(final int key, final int value); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void forEach(final BiIntIntConsumer action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final int value = curr.getValueAcquire(); ++ ++ action.accept(curr.key, value); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all keys contained within this map. ++ * @param action The specified consumer. ++ */ ++ public void forEachKey(final IntConsumer action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ action.accept(curr.key); ++ } ++ } ++ } ++ ++ /** ++ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. ++ * @param action The specified consumer. ++ */ ++ public void forEachValue(final IntConsumer action) { ++ Validate.notNull(action, "Null action"); ++ ++ final TableEntry[] table = this.getTableAcquire(); ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { ++ final int value = curr.getValueAcquire(); ++ ++ action.accept(value); ++ } ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int get(final int key) { ++ final TableEntry entry = this.getEntryForOpaque(key); ++ return entry == null ? 0 : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public boolean containsKey(final int key) { ++ final TableEntry entry = this.getEntryForOpaque(key); ++ return entry != null; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int getOrDefault(final int key, final int defaultValue) { ++ final TableEntry entry = this.getEntryForOpaque(key); ++ ++ return entry == null ? defaultValue : entry.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int size() { ++ return this.getSizeAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public boolean isEmpty() { ++ return this.getSizeAcquire() == 0; ++ } ++ ++ /* Non-MT-Safe */ ++ ++ protected int threshold; ++ ++ protected final void checkResize(final int minCapacity) { ++ if (minCapacity <= this.threshold || this.threshold < 0) { ++ return; ++ } ++ ++ final TableEntry[] table = this.getTablePlain(); ++ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); ++ if (newCapacity < 0) { ++ newCapacity = MAXIMUM_CAPACITY; ++ } ++ if (newCapacity <= table.length) { ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ return; ++ } ++ newCapacity = table.length << 1; ++ } ++ ++ //noinspection unchecked ++ final TableEntry[] newTable = new TableEntry[newCapacity]; ++ final int indexMask = newCapacity - 1; ++ ++ for (int i = 0, len = table.length; i < len; ++i) { ++ for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { ++ final int key = entry.key; ++ final int hash = SWMRInt2IntHashTable.getHash(key); ++ final int index = hash & indexMask; ++ ++ /* we need to create a new entry since there could be reading threads */ ++ final TableEntry insert = new TableEntry(key, entry.getValuePlain()); ++ ++ final TableEntry prev = newTable[index]; ++ ++ newTable[index] = insert; ++ insert.setNextPlain(prev); ++ } ++ } ++ ++ if (newCapacity == MAXIMUM_CAPACITY) { ++ this.threshold = -1; /* No more resizing */ ++ } else { ++ this.threshold = getTargetCapacity(newCapacity, this.loadFactor); ++ } ++ this.setTableRelease(newTable); /* use release to publish entries in table */ ++ } ++ ++ protected final int addToSize(final int num) { ++ final int newSize = this.getSizePlain() + num; ++ ++ this.setSizeOpaque(newSize); ++ this.checkResize(newSize); ++ ++ return newSize; ++ } ++ ++ protected final int removeFromSize(final int num) { ++ final int newSize = this.getSizePlain() - num; ++ ++ this.setSizeOpaque(newSize); ++ ++ return newSize; ++ } ++ ++ protected final int put(final int key, final int value, final boolean onlyIfAbsent) { ++ final TableEntry[] table = this.getTablePlain(); ++ final int hash = SWMRInt2IntHashTable.getHash(key); ++ final int index = hash & (table.length - 1); ++ ++ final TableEntry head = table[index]; ++ if (head == null) { ++ final TableEntry insert = new TableEntry(key, value); ++ ArrayUtil.setRelease(table, index, insert); ++ this.addToSize(1); ++ return 0; ++ } ++ ++ for (TableEntry curr = head;;) { ++ if (key == curr.key) { ++ if (onlyIfAbsent) { ++ return curr.getValuePlain(); ++ } ++ ++ final int currVal = curr.getValuePlain(); ++ curr.setValueRelease(value); ++ return currVal; ++ } ++ ++ final TableEntry next = curr.getNextPlain(); ++ if (next != null) { ++ curr = next; ++ continue; ++ } ++ ++ final TableEntry insert = new TableEntry(key, value); ++ ++ curr.setNextRelease(insert); ++ this.addToSize(1); ++ return 0; ++ } ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int put(final int key, final int value) { ++ return this.put(key, value, false); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int putIfAbsent(final int key, final int value) { ++ return this.put(key, value, true); ++ } ++ ++ protected final int remove(final int key, final int hash) { ++ final TableEntry[] table = this.getTablePlain(); ++ final int index = (table.length - 1) & hash; ++ ++ final TableEntry head = table[index]; ++ if (head == null) { ++ return 0; ++ } ++ ++ if (head.key == key) { ++ ArrayUtil.setRelease(table, index, head.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return head.getValuePlain(); ++ } ++ ++ for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (key == curr.key) { ++ prev.setNextRelease(curr.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return curr.getValuePlain(); ++ } ++ } ++ ++ return 0; ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int remove(final int key) { ++ return this.remove(key, SWMRInt2IntHashTable.getHash(key)); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public void putAll(final SWMRInt2IntHashTable map) { ++ Validate.notNull(map, "Null map"); ++ ++ final int size = map.size(); ++ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ ++ map.forEach(this::put); ++ } ++ ++ /** ++ * {@inheritDoc} ++ *

++ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself ++ * is release ordered, that is, after the clear operation is performed a release fence is performed. ++ *

++ */ ++ public void clear() { ++ Arrays.fill(this.getTablePlain(), null); ++ this.setSizeRelease(0); ++ } ++ ++ public static final class TableEntry { ++ ++ protected final int key; ++ protected int value; ++ ++ protected TableEntry next; ++ ++ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); ++ ++ /* value */ ++ ++ protected final int getValuePlain() { ++ //noinspection unchecked ++ return (int)VALUE_HANDLE.get(this); ++ } ++ ++ protected final int getValueAcquire() { ++ //noinspection unchecked ++ return (int)VALUE_HANDLE.getAcquire(this); ++ } ++ ++ protected final void setValueRelease(final int to) { ++ VALUE_HANDLE.setRelease(this, to); ++ } ++ ++ /* next */ ++ ++ protected final TableEntry getNextPlain() { ++ //noinspection unchecked ++ return (TableEntry)NEXT_HANDLE.get(this); ++ } ++ ++ protected final TableEntry getNextOpaque() { ++ //noinspection unchecked ++ return (TableEntry)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ protected final void setNextPlain(final TableEntry next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextRelease(final TableEntry next) { ++ NEXT_HANDLE.setRelease(this, next); ++ } ++ ++ protected TableEntry(final int key, final int value) { ++ this.key = key; ++ this.value = value; ++ } ++ ++ public int getKey() { ++ return this.key; ++ } ++ ++ public int getValue() { ++ return this.getValueAcquire(); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ public int setValue(final int value) { ++ final int curr = this.getValuePlain(); ++ ++ this.setValueRelease(value); ++ return curr; ++ } ++ ++ protected static int hash(final int key, final int value) { ++ return SWMRInt2IntHashTable.getHash(key) ^ SWMRInt2IntHashTable.getHash(value); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public int hashCode() { ++ return hash(this.key, this.getValueAcquire()); ++ } ++ ++ /** ++ * {@inheritDoc} ++ */ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof TableEntry)) { ++ return false; ++ } ++ final TableEntry other = (TableEntry)obj; ++ final int otherKey = other.getKey(); ++ final int thisKey = this.getKey(); ++ final int otherValue = other.getValueAcquire(); ++ final int thisVal = this.getValueAcquire(); ++ return (thisKey == otherKey) && (thisVal == otherValue); ++ } ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java +index 1e98f778ffa0a7bb00ebccaaa8bde075183e41f0..aebe82cbe8bc20e5f4260a871d7b620e5092b2c9 100644 +--- a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java ++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java +@@ -534,6 +534,44 @@ public class SWMRLong2ObjectHashTable { + return null; + } + ++ protected final V remove(final long key, final int hash, final V expect) { ++ final TableEntry[] table = this.getTablePlain(); ++ final int index = (table.length - 1) & hash; ++ ++ final TableEntry head = table[index]; ++ if (head == null) { ++ return null; ++ } ++ ++ if (head.key == key) { ++ final V val = head.value; ++ if (val == expect || val.equals(expect)) { ++ ArrayUtil.setRelease(table, index, head.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return head.getValuePlain(); ++ } else { ++ return null; ++ } ++ } ++ ++ for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { ++ if (key == curr.key) { ++ final V val = curr.value; ++ if (val == expect || val.equals(expect)) { ++ prev.setNextRelease(curr.getNextPlain()); ++ this.removeFromSize(1); ++ ++ return curr.getValuePlain(); ++ } else { ++ return null; ++ } ++ } ++ } ++ ++ return null; ++ } ++ + /** + * {@inheritDoc} + */ +@@ -541,6 +579,10 @@ public class SWMRLong2ObjectHashTable { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); + } + ++ public boolean remove(final long key, final V expect) { ++ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null; ++ } ++ + /** + * {@inheritDoc} + */ +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f579ad58ea7db20d6d7b89abbab3a4dfadaaeaee +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java +@@ -0,0 +1,534 @@ ++package ca.spottedleaf.concurrentutil.scheduler; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.set.LinkedSortedSet; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.BitSet; ++import java.util.Comparator; ++import java.util.PriorityQueue; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.LockSupport; ++import java.util.function.BooleanSupplier; ++ ++public class SchedulerThreadPool { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; ++ ++ private static final Comparator TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> { ++ final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart); ++ if (timeCompare != 0) { ++ return timeCompare; ++ } ++ ++ return Long.compare(t1.id, t2.id); ++ }; ++ ++ private final TickThreadRunner[] runners; ++ private final Thread[] threads; ++ private final LinkedSortedSet awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME); ++ private final PriorityQueue queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME); ++ private final BitSet idleThreads; ++ ++ private final Object scheduleLock = new Object(); ++ ++ private volatile boolean halted; ++ ++ public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) { ++ final BitSet idleThreads = new BitSet(threads); ++ for (int i = 0; i < threads; ++i) { ++ idleThreads.set(i); ++ } ++ this.idleThreads = idleThreads; ++ ++ final TickThreadRunner[] runners = new TickThreadRunner[threads]; ++ final Thread[] t = new Thread[threads]; ++ for (int i = 0; i < threads; ++i) { ++ runners[i] = new TickThreadRunner(i, this); ++ t[i] = threadFactory.newThread(runners[i]); ++ } ++ ++ this.threads = t; ++ this.runners = runners; ++ } ++ ++ /** ++ * Starts the threads in this pool. ++ */ ++ public void start() { ++ for (final Thread thread : this.threads) { ++ thread.start(); ++ } ++ } ++ ++ /** ++ * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die. ++ * ++ * @param sync Whether to wait for the scheduler threads to die. ++ * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die. ++ * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout. ++ * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time. ++ */ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ this.halted = true; ++ for (final Thread thread : this.threads) { ++ // force response to halt ++ LockSupport.unpark(thread); ++ } ++ final long time = System.nanoTime(); ++ if (sync) { ++ // start at 10 * 0.5ms -> 5ms ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ boolean allDead = true; ++ for (final Thread thread : this.threads) { ++ if (thread.isAlive()) { ++ allDead = false; ++ break; ++ } ++ } ++ if (allDead) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Returns an array of the underlying scheduling threads. ++ */ ++ public Thread[] getThreads() { ++ return this.threads.clone(); ++ } ++ ++ private void insertFresh(final SchedulableTick task) { ++ final TickThreadRunner[] runners = this.runners; ++ ++ final int firstIdleThread = this.idleThreads.nextSetBit(0); ++ ++ if (firstIdleThread != -1) { ++ // push to idle thread ++ this.idleThreads.clear(firstIdleThread); ++ final TickThreadRunner runner = runners[firstIdleThread]; ++ task.awaitingLink = this.awaiting.addLast(task); ++ runner.acceptTask(task); ++ return; ++ } ++ ++ // try to replace the last awaiting task ++ final SchedulableTick last = this.awaiting.last(); ++ ++ if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) { ++ // need to replace the last task ++ this.awaiting.pollLast(); ++ last.awaitingLink = null; ++ task.awaitingLink = this.awaiting.addLast(task); ++ // need to add task to queue to be picked up later ++ this.queued.add(last); ++ ++ final TickThreadRunner runner = last.ownedBy; ++ runner.replaceTask(task); ++ ++ return; ++ } ++ ++ // add to queue, will be picked up later ++ this.queued.add(task); ++ } ++ ++ private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) { ++ if (!this.awaiting.remove(tick.awaitingLink)) { ++ throw new IllegalStateException("Task is not in awaiting"); ++ } ++ tick.awaitingLink = null; ++ } ++ ++ private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) { ++ if (reschedule != null) { ++ this.queued.add(reschedule); ++ } ++ final SchedulableTick ret = this.queued.poll(); ++ if (ret == null) { ++ this.idleThreads.set(runner.id); ++ } else { ++ ret.awaitingLink = this.awaiting.addLast(ret); ++ } ++ ++ return ret; ++ } ++ ++ public void schedule(final SchedulableTick task) { ++ synchronized (this.scheduleLock) { ++ if (!task.tryMarkScheduled()) { ++ throw new IllegalStateException("Task " + task + " is already scheduled or cancelled"); ++ } ++ ++ task.schedulerOwnedBy = this; ++ ++ this.insertFresh(task); ++ } ++ } ++ ++ public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) { ++ synchronized (this.scheduleLock) { ++ if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) { ++ return false; ++ } ++ if (this.queued.remove(task)) { ++ task.setScheduledStart(newStart); ++ this.queued.add(task); ++ return true; ++ } ++ if (task.awaitingLink != null) { ++ this.awaiting.remove(task.awaitingLink); ++ task.awaitingLink = null; ++ ++ // re-queue task ++ task.setScheduledStart(newStart); ++ this.queued.add(task); ++ ++ // now we need to replace the task the runner was waiting for ++ final TickThreadRunner runner = task.ownedBy; ++ final SchedulableTick replace = this.queued.poll(); ++ ++ // replace cannot be null, since we have added a task to queued ++ if (replace != task) { ++ runner.replaceTask(replace); ++ } ++ ++ return true; ++ } ++ ++ return false; ++ } ++ } ++ ++ /** ++ * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled ++ * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing. ++ */ ++ public Boolean tryRetire(final SchedulableTick task) { ++ if (task.schedulerOwnedBy != this) { ++ return null; ++ } ++ ++ synchronized (this.scheduleLock) { ++ if (this.queued.remove(task)) { ++ // cancelled, and no runner owns it - so return ++ return Boolean.TRUE; ++ } ++ if (task.awaitingLink != null) { ++ this.awaiting.remove(task.awaitingLink); ++ task.awaitingLink = null; ++ // here we need to replace the task the runner was waiting for ++ final TickThreadRunner runner = task.ownedBy; ++ final SchedulableTick replace = this.queued.poll(); ++ ++ if (replace == null) { ++ // nothing to replace with, set to idle ++ this.idleThreads.set(runner.id); ++ runner.forceIdle(); ++ } else { ++ runner.replaceTask(replace); ++ } ++ ++ return Boolean.TRUE; ++ } ++ ++ // could not find it in queue ++ return task.tryMarkCancelled() ? Boolean.FALSE : null; ++ } ++ } ++ ++ public void notifyTasks(final SchedulableTick task) { ++ // Not implemented ++ } ++ ++ /** ++ * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}. ++ *

++ * A tickable task is expected to run on a fixed interval, which is determined by ++ * the {@link SchedulerThreadPool}. ++ *

++ *

++ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of ++ * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain ++ * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take ++ * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking. ++ *

++ *

++ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never ++ * invoked in parallel. ++ * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)} ++ * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to ++ * parse intermediate tasks. ++ *

++ */ ++ public static abstract class SchedulableTick { ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ public final long id = ID_GENERATOR.getAndIncrement(); ++ ++ private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0; ++ private static final int SCHEDULE_STATE_SCHEDULED = 1; ++ private static final int SCHEDULE_STATE_CANCELLED = 2; ++ ++ private final AtomicInteger scheduled = new AtomicInteger(); ++ private SchedulerThreadPool schedulerOwnedBy; ++ private long scheduledStart = DEADLINE_NOT_SET; ++ private TickThreadRunner ownedBy; ++ ++ private LinkedSortedSet.Link awaitingLink; ++ ++ private boolean tryMarkScheduled() { ++ return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED); ++ } ++ ++ private boolean tryMarkCancelled() { ++ return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED); ++ } ++ ++ private boolean isScheduled() { ++ return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED; ++ } ++ ++ protected final long getScheduledStart() { ++ return this.scheduledStart; ++ } ++ ++ /** ++ * If this task is scheduled, then this may only be invoked during {@link #runTick()}, ++ * and {@link #runTasks(BooleanSupplier)} ++ */ ++ protected final void setScheduledStart(final long value) { ++ this.scheduledStart = value; ++ } ++ ++ /** ++ * Executes the tick. ++ *

++ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of ++ * the next tick. ++ *

++ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise. ++ */ ++ public abstract boolean runTick(); ++ ++ /** ++ * Returns whether this task has any intermediate tasks that can be executed. ++ */ ++ public abstract boolean hasTasks(); ++ ++ /** ++ * Returns {@code null} if this task should not be scheduled, otherwise returns ++ * {@code Boolean.TRUE} if there are more intermediate tasks to execute and ++ * {@code Boolean.FALSE} if there are no more intermediate tasks to execute. ++ */ ++ public abstract Boolean runTasks(final BooleanSupplier canContinue); ++ ++ @Override ++ public String toString() { ++ return "SchedulableTick:{" + ++ "class=" + this.getClass().getName() + "," + ++ "scheduled_state=" + this.scheduled.get() + "," ++ + "}"; ++ } ++ } ++ ++ private static final class TickThreadRunner implements Runnable { ++ ++ /** ++ * There are no tasks in this thread's runqueue, so it is parked. ++ *

++ * stateTarget = null ++ *

++ */ ++ private static final int STATE_IDLE = 0; ++ ++ /** ++ * The runner is waiting to tick a task, as it has no intermediate tasks to execute. ++ *

++ * stateTarget = the task awaiting tick ++ *

++ */ ++ private static final int STATE_AWAITING_TICK = 1; ++ ++ /** ++ * The runner is executing a tick for one of the tasks that was in its runqueue. ++ *

++ * stateTarget = the task being ticked ++ *

++ */ ++ private static final int STATE_EXECUTING_TICK = 2; ++ ++ public final int id; ++ public final SchedulerThreadPool scheduler; ++ ++ private volatile Thread thread; ++ private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE); ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class); ++ ++ private void setStatePlain(final TickThreadRunnerState state) { ++ STATE_HANDLE.set(this, state); ++ } ++ ++ private void setStateOpaque(final TickThreadRunnerState state) { ++ STATE_HANDLE.setOpaque(this, state); ++ } ++ ++ private void setStateVolatile(final TickThreadRunnerState state) { ++ STATE_HANDLE.setVolatile(this, state); ++ } ++ ++ private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {} ++ ++ public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) { ++ this.id = id; ++ this.scheduler = scheduler; ++ } ++ ++ private Thread getRunnerThread() { ++ return this.thread; ++ } ++ ++ private void acceptTask(final SchedulableTick task) { ++ if (task.ownedBy != null) { ++ throw new IllegalStateException("Already owned by another runner"); ++ } ++ task.ownedBy = this; ++ final TickThreadRunnerState state = this.state; ++ if (state.state != STATE_IDLE) { ++ throw new IllegalStateException("Cannot accept task in state " + state); ++ } ++ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); ++ LockSupport.unpark(this.getRunnerThread()); ++ } ++ ++ private void replaceTask(final SchedulableTick task) { ++ final TickThreadRunnerState state = this.state; ++ if (state.state != STATE_AWAITING_TICK) { ++ throw new IllegalStateException("Cannot replace task in state " + state); ++ } ++ if (task.ownedBy != null) { ++ throw new IllegalStateException("Already owned by another runner"); ++ } ++ task.ownedBy = this; ++ ++ state.stateTarget.ownedBy = null; ++ ++ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); ++ LockSupport.unpark(this.getRunnerThread()); ++ } ++ ++ private void forceIdle() { ++ final TickThreadRunnerState state = this.state; ++ if (state.state != STATE_AWAITING_TICK) { ++ throw new IllegalStateException("Cannot replace task in state " + state); ++ } ++ state.stateTarget.ownedBy = null; ++ this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE)); ++ // no need to unpark ++ } ++ ++ private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) { ++ synchronized (this.scheduler.scheduleLock) { ++ if (this.state != state) { ++ return false; ++ } ++ this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK)); ++ this.scheduler.takeTask(this, task); ++ return true; ++ } ++ } ++ ++ private void returnTask(final SchedulableTick task, final boolean reschedule) { ++ synchronized (this.scheduler.scheduleLock) { ++ task.ownedBy = null; ++ ++ final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null); ++ if (newWait == null) { ++ this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE)); ++ } else { ++ if (newWait.ownedBy != null) { ++ throw new IllegalStateException("Already owned by another runner"); ++ } ++ newWait.ownedBy = this; ++ this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK)); ++ } ++ } ++ } ++ ++ @Override ++ public void run() { ++ this.thread = Thread.currentThread(); ++ ++ main_state_loop: ++ for (;;) { ++ final TickThreadRunnerState startState = this.state; ++ final int startStateType = startState.state; ++ final SchedulableTick startStateTask = startState.stateTarget; ++ ++ if (this.scheduler.halted) { ++ return; ++ } ++ ++ switch (startStateType) { ++ case STATE_IDLE: { ++ while (this.state.state == STATE_IDLE) { ++ LockSupport.park(); ++ if (this.scheduler.halted) { ++ return; ++ } ++ } ++ continue main_state_loop; ++ } ++ ++ case STATE_AWAITING_TICK: { ++ final long deadline = startStateTask.getScheduledStart(); ++ for (;;) { ++ if (this.state != startState) { ++ continue main_state_loop; ++ } ++ final long diff = deadline - System.nanoTime(); ++ if (diff <= 0L) { ++ break; ++ } ++ LockSupport.parkNanos(startState, diff); ++ if (this.scheduler.halted) { ++ return; ++ } ++ } ++ ++ if (!this.takeTask(startState, startStateTask)) { ++ continue main_state_loop; ++ } ++ ++ // TODO exception handling ++ final boolean reschedule = startStateTask.runTick(); ++ ++ this.returnTask(startStateTask, reschedule); ++ ++ continue main_state_loop; ++ } ++ ++ case STATE_EXECUTING_TICK: { ++ throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread"); ++ } ++ ++ default: { ++ throw new IllegalStateException("Unknown state: " + startState); ++ } ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java +@@ -0,0 +1,60 @@ ++package ca.spottedleaf.concurrentutil.util; ++ ++public final class TimeUtil { ++ ++ /* ++ * The comparator is not a valid comparator for every long value. To prove where it is valid, see below. ++ * ++ * For reflexivity, we have that x - x = 0. We then have that for any long value x that ++ * compareTimes(x, x) == 0, as expected. ++ * ++ * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE. ++ * So, the difference between any times x and y must not be equal to Long.MIN_VALUE. ++ * ++ * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that ++ * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values, ++ * this holds as long as a + b <= Long.MAX_VALUE. ++ * ++ * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have ++ * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE. ++ * ++ * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps ++ * c and d. ++ */ ++ ++ /** ++ * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which ++ * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2. ++ */ ++ public static int compareTimes(final long t1, final long t2) { ++ final long diff = t1 - t2; ++ ++ // HD, Section 2-7 ++ return (int) ((diff >> 63) | (-diff >>> 63)); ++ } ++ ++ public static long getGreatestTime(final long t1, final long t2) { ++ final long diff = t1 - t2; ++ return diff < 0L ? t2 : t1; ++ } ++ ++ public static long getLeastTime(final long t1, final long t2) { ++ final long diff = t1 - t2; ++ return diff > 0L ? t2 : t1; ++ } ++ ++ public static long clampTime(final long value, final long min, final long max) { ++ final long diffMax = value - max; ++ final long diffMin = value - min; ++ ++ if (diffMax > 0L) { ++ return max; ++ } ++ if (diffMin < 0L) { ++ return min; ++ } ++ return value; ++ } ++ ++ private TimeUtil() {} ++} +diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java +new file mode 100644 +index 0000000000000000000000000000000000000000..14a4778f7913b849fabbd772f9cb8a0bc5a6ed6c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java +@@ -0,0 +1,58 @@ ++package ca.spottedleaf.leafprofiler; ++ ++import ca.spottedleaf.concurrentutil.map.SWMRInt2IntHashTable; ++import java.util.Arrays; ++ ++public final class LProfileGraph { ++ ++ public static final int ROOT_NODE = 0; ++ ++ // volatile required for correct publishing after resizing ++ private volatile SWMRInt2IntHashTable[] nodes = new SWMRInt2IntHashTable[16]; ++ private int nodeCount; ++ ++ public LProfileGraph() { ++ this.nodes[ROOT_NODE] = new SWMRInt2IntHashTable(); ++ this.nodeCount = 1; ++ } ++ ++ private int createNode(final int parent, final int type) { ++ synchronized (this) { ++ SWMRInt2IntHashTable[] nodes = this.nodes; ++ ++ final SWMRInt2IntHashTable node = nodes[parent]; ++ ++ final int newNode = this.nodeCount; ++ final int prev = node.putIfAbsent(type, newNode); ++ ++ if (prev != 0) { ++ // already exists ++ return prev; ++ } ++ ++ // insert new node ++ ++this.nodeCount; ++ ++ if (newNode >= nodes.length) { ++ this.nodes = nodes = Arrays.copyOf(nodes, nodes.length * 2); ++ } ++ ++ nodes[newNode] = new SWMRInt2IntHashTable(); ++ ++ return newNode; ++ } ++ } ++ ++ public int getOrCreateNode(final int parent, final int type) { ++ // note: requires parent node to exist ++ final SWMRInt2IntHashTable[] nodes = this.nodes; ++ ++ final int mapping = nodes[parent].get(type); ++ ++ if (mapping != 0) { ++ return mapping; ++ } ++ ++ return this.createNode(parent, type); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ffa32c1eae22bda371dd1d0318cc7c587f8e5a5c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java +@@ -0,0 +1,59 @@ ++package ca.spottedleaf.leafprofiler; ++ ++import java.util.Arrays; ++import java.util.concurrent.ConcurrentHashMap; ++ ++public final class LProfilerRegistry { ++ ++ // volatile required to ensure correct publishing when resizing ++ private volatile ProfilerEntry[] typesById = new ProfilerEntry[16]; ++ private int totalEntries; ++ private final ConcurrentHashMap nameToEntry = new ConcurrentHashMap<>(); ++ ++ public LProfilerRegistry() { ++ ++ } ++ ++ public ProfilerEntry getById(final int id) { ++ final ProfilerEntry[] entries = this.typesById; ++ ++ return id < 0 || id >= entries.length ? null : entries[id]; ++ } ++ ++ public ProfilerEntry getByName(final String name) { ++ return this.nameToEntry.get(name); ++ } ++ ++ public int createType(final ProfileType type, final String name) { ++ synchronized (this) { ++ final int id = this.totalEntries; ++ ++ final ProfilerEntry ret = new ProfilerEntry(type, name, id); ++ ++ final ProfilerEntry prev = this.nameToEntry.putIfAbsent(name, ret); ++ ++ if (prev != null) { ++ throw new IllegalStateException("Entry already exists: " + prev); ++ } ++ ++ ++this.totalEntries; ++ ++ ProfilerEntry[] entries = this.typesById; ++ ++ if (id >= entries.length) { ++ this.typesById = entries = Arrays.copyOf(entries, entries.length * 2); ++ } ++ ++ // should be opaque, but I don't think that matters here. ++ entries[id] = ret; ++ ++ return id; ++ } ++ } ++ ++ public static enum ProfileType { ++ TIMER, COUNTER ++ } ++ ++ public static record ProfilerEntry(ProfileType type, String name, int id) {} ++} +diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ad8c590fe7479fcb3c7ff5dc3ac3a4d6f33c5938 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java +@@ -0,0 +1,61 @@ ++package ca.spottedleaf.leafprofiler; ++ ++import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import java.util.Arrays; ++ ++public final class LeafProfiler { ++ ++ public final LProfilerRegistry registry; ++ public final LProfileGraph graph; ++ ++ private long[] data; ++ private final IntArrayFIFOQueue callStack = new IntArrayFIFOQueue(); ++ private int topOfStack = LProfileGraph.ROOT_NODE; ++ private final LongArrayFIFOQueue timerStack = new LongArrayFIFOQueue(); ++ private long lastTimerStart = 0L; ++ ++ public LeafProfiler(final LProfilerRegistry registry, final LProfileGraph graph) { ++ this.registry = registry; ++ this.graph = graph; ++ } ++ ++ private long[] resizeData(final long[] old, final int least) { ++ return this.data = Arrays.copyOf(old, Math.max(old.length * 2, least * 2)); ++ } ++ ++ private void incrementDirect(final int nodeId, final long count) { ++ final long[] data = this.data; ++ if (nodeId >= data.length) { ++ this.resizeData(data, nodeId)[nodeId] += count; ++ } else { ++ data[nodeId] += count; ++ } ++ } ++ ++ public void incrementCounter(final int type, final long count) { ++ // this is supposed to be an optimised version of startTimer then stopTimer ++ final int node = this.graph.getOrCreateNode(this.topOfStack, type); ++ this.incrementDirect(node, count); ++ } ++ ++ public void startTimer(final int type, final long startTime) { ++ final int parentNode = this.topOfStack; ++ final int newNode = this.graph.getOrCreateNode(parentNode, type); ++ this.callStack.enqueue(parentNode); ++ this.topOfStack = newNode; ++ ++ this.timerStack.enqueue(this.lastTimerStart); ++ this.lastTimerStart = startTime; ++ } ++ ++ public void stopTimer(final int type, final long endTime) { ++ final int currentNode = this.topOfStack; ++ this.topOfStack = this.callStack.dequeueLastInt(); ++ ++ final long lastStart = this.lastTimerStart; ++ this.lastTimerStart = this.timerStack.dequeueLastLong(); ++ ++ this.incrementDirect(currentNode, endTime - lastStart); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 4b002e8b75d117b726b0de274a76d3596fce015b..897cb94abf7b53da8ba7cda5135b6580aa2d9824 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); ++ Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -611,7 +611,7 @@ public class Metrics { + } else { + paperVersion = "unknown"; + } +- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); ++ metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> paperVersion)); // Tuinity - we have our own bstats page + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +index 4f3670b2bdb8b1b252e9f074a6af56a018a8c465..bb3df6a4d8b87219c3c0406c56428c28d5f9ab4e 100644 +--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -179,11 +179,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo + return; + } + +- if (!Bukkit.isPrimaryThread()) { +- // Plugins? +- MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); +- return; +- } ++ // Paper - region threading + + LevelChunk chunk = chunkPacketInfo.getChunk(); + int x = chunk.getPos().x; +diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +index 309fe1162db195c7c3c94d785d6aa2700e42b08a..70db79d37257dedada9f55b3cf1127451c151072 100644 +--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java ++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +@@ -97,7 +97,7 @@ public final class ChatProcessor { + final CraftPlayer player = this.player.getBukkitEntity(); + final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server)); + this.post(ae); +- if (listenersOnSyncEvent) { ++ if (false && listenersOnSyncEvent) { // Paper - region threading + final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients()); + se.setCancelled(ae.isCancelled()); // propagate cancelled state + this.queueIfAsyncOrRunImmediately(new Waitable() { +@@ -177,7 +177,7 @@ public final class ChatProcessor { + ae.setCancelled(cancelled); // propagate cancelled state + this.post(ae); + final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList()); +- if (listenersOnSyncEvent) { ++ if (false && listenersOnSyncEvent) { // Paper - region threading + this.queueIfAsyncOrRunImmediately(new Waitable() { + @Override + protected Void evaluate() { +diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +index 6df1948b1204a7288ecb7238b6fc2a733f7d25b3..7ac1ca0e358a38cf5d1e7f7cdc7383ca9c7df6f2 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java ++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +@@ -91,6 +91,9 @@ public final class ChunkSystem { + for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { + chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z); + } ++ // Paper start - threaded regions ++ level.regioniser.addChunk(holder.pos.x, holder.pos.z); ++ // Paper end - threaded regions + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { +@@ -98,6 +101,9 @@ public final class ChunkSystem { + for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { + chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); + } ++ // Paper start - threaded regions ++ level.regioniser.removeChunk(holder.pos.x, holder.pos.z); ++ // Paper end - threaded regions + } + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { +@@ -109,19 +115,19 @@ public final class ChunkSystem { + } + + public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { +- chunk.level.getChunkSource().tickingChunks.add(chunk); ++ // Paper - region threading + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { +- chunk.level.getChunkSource().tickingChunks.remove(chunk); ++ // Paper - region threading + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- chunk.level.getChunkSource().entityTickingChunks.add(chunk); ++ chunk.level.getCurrentWorldData().addEntityTickingChunks(chunk); // Paper - region threading + } + + public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- chunk.level.getChunkSource().entityTickingChunks.remove(chunk); ++ chunk.level.getCurrentWorldData().removeEntityTickingChunk(chunk); // Paper - region threading + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { +diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java +index a4d58352eebed11fafde8c381afe3572893b8f8f..b9b3ab0bcddbbe485bb138bfd4882a21067f8bde 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java ++++ b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java +@@ -231,14 +231,14 @@ public class RegionisedPlayerChunkLoader { + + public void tick() { + TickThread.ensureTickThread("Cannot tick player chunk loader async"); +- for (final ServerPlayer player : this.world.players()) { ++ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Paper - region threding + player.chunkLoader.update(); + } + } + + public void tickMidTick() { + final long time = System.nanoTime(); +- for (final ServerPlayer player : this.world.players()) { ++ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Paper - region threading + player.chunkLoader.midTickUpdate(time); + } + } +diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +index 61c170555c8854b102c640b0b6a615f9f732edbf..576e48f68861b817bcd94252e1fd587e31008458 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java ++++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +@@ -187,7 +187,12 @@ public final class EntityLookup implements LevelEntityGetter { + + @Override + public Iterable getAll() { +- return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size()); ++ // Paper start - region threading ++ synchronized (this.accessibleEntities) { ++ Entity[] iterate = java.util.Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size()); ++ return new ArrayIterable<>(iterate, 0, iterate.length); ++ } ++ // Paper end - region threading + } + + @Override +@@ -261,7 +266,9 @@ public final class EntityLookup implements LevelEntityGetter { + if (newVisibility.ordinal() > oldVisibility.ordinal()) { + // status upgrade + if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { ++ synchronized (this.accessibleEntities) { // Paper - region threading + this.accessibleEntities.add(entity); ++ } // Paper - region threading + EntityLookup.this.worldCallback.onTrackingStart(entity); + } + +@@ -275,7 +282,9 @@ public final class EntityLookup implements LevelEntityGetter { + } + + if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { ++ synchronized (this.accessibleEntities) { // Paper - region threading + this.accessibleEntities.remove(entity); ++ } // Paper - region threading + EntityLookup.this.worldCallback.onTrackingEnd(entity); + } + } +@@ -385,6 +394,8 @@ public final class EntityLookup implements LevelEntityGetter { + + entity.setLevelCallback(new EntityCallback(entity)); + ++ this.world.getCurrentWorldData().addEntity(entity); // Paper - region threading ++ + this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); + + return true; +@@ -407,6 +418,7 @@ public final class EntityLookup implements LevelEntityGetter { + LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); + } + } ++ + entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE; + + this.entityByLock.writeLock(); +@@ -823,6 +835,9 @@ public final class EntityLookup implements LevelEntityGetter { + EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); + + this.entity.setLevelCallback(NoOpCallback.INSTANCE); ++ ++ // only AFTER full removal callbacks, so that thread checking will work. // Paper - region threading ++ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Paper - region threading + } + } + +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index c6d20bc2f0eab737338db6b88dacb63f0decb66c..8f44efc736055aaf85e4f9068618e911bae0c30c 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -3,7 +3,6 @@ package io.papermc.paper.chunk.system.scheduling; + import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; + import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +-import co.aikar.timings.Timing; + import com.google.common.collect.ImmutableList; + import com.google.gson.JsonArray; + import com.google.gson.JsonObject; +@@ -19,10 +18,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntMap; + import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; + import it.unimi.dsi.fastutil.longs.Long2ObjectMap; + import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; + import it.unimi.dsi.fastutil.longs.LongArrayList; + import it.unimi.dsi.fastutil.longs.LongIterator; + import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; + import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; + import net.minecraft.nbt.CompoundTag; + import io.papermc.paper.chunk.system.ChunkSystem; + import net.minecraft.server.MinecraftServer; +@@ -34,8 +35,6 @@ import net.minecraft.server.level.TicketType; + import net.minecraft.util.SortedArraySet; + import net.minecraft.util.Unit; + import net.minecraft.world.level.ChunkPos; +-import net.minecraft.world.level.chunk.ChunkAccess; +-import net.minecraft.world.level.chunk.ChunkStatus; + import org.bukkit.plugin.Plugin; + import org.slf4j.Logger; + import java.io.IOException; +@@ -54,6 +53,13 @@ import java.util.concurrent.locks.LockSupport; + import java.util.concurrent.locks.ReentrantLock; + import java.util.function.Predicate; + ++// Paper start - region threading ++import io.papermc.paper.threadedregions.RegionisedServer; ++import io.papermc.paper.threadedregions.ThreadedRegioniser; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++// Paper end - region threading ++ + public final class ChunkHolderManager { + + private static final Logger LOGGER = LogUtils.getClassLogger(); +@@ -63,40 +69,201 @@ public final class ChunkHolderManager { + public static final int ENTITY_TICKING_TICKET_LEVEL = 31; + public static final int MAX_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; // inclusive + +- private static final long NO_TIMEOUT_MARKER = -1L; ++ // Paper start - region threading ++ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; ++ private static final long PROBE_MARKER = Long.MIN_VALUE + 1; ++ // Paper end - region threading + +- final ReentrantLock ticketLock = new ReentrantLock(); ++ public final ReentrantLock ticketLock = new ReentrantLock(); // Paper - region threading + + private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f); +- private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f); +- // what a disaster of a name +- // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick +- private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); ++ // Paper - region threading + private final ServerLevel world; + private final ChunkTaskScheduler taskScheduler; +- private long currentTick; + +- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); +- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { +- if (c1 == c2) { +- return 0; ++ // Paper start - region threading ++ public static final class HolderManagerRegionData { ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } ++ ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ private long currentTick; ++ private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f); ++ // what a disaster of a name ++ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick ++ private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); ++ ++ // special region threading fields ++ // this field contains chunk holders that were created in addTicketAtLevel ++ // because the chunk holders were created without a reliable unload hook (i.e creation for entity/poi loading, ++ // which always check for unload after their tasks finish) we need to do that ourselves later ++ private final ReferenceOpenHashSet specialCaseUnload = new ReferenceOpenHashSet<>(); ++ ++ public void merge(final HolderManagerRegionData into, final long tickOffset) { ++ // Order doesn't really matter for the pending full update... ++ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); ++ ++ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating ++ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant ++ // addition to every entry will not affect compareTo). ++ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { ++ holder.lastAutoSave += tickOffset; ++ into.autoSaveQueue.add(holder); ++ } ++ ++ final long chunkManagerTickOffset = into.currentTick - this.currentTick; ++ for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = iterator.next(); ++ final SortedArraySet> oldTickets = entry.getValue(); ++ final SortedArraySet> newTickets = SortedArraySet.create(Math.max(4, oldTickets.size() + 1)); ++ for (final Ticket ticket : oldTickets) { ++ newTickets.add( ++ new Ticket(ticket.getType(), ticket.getTicketLevel(), ticket.key, ++ ticket.removalTick == NO_TIMEOUT_MARKER ? NO_TIMEOUT_MARKER : ticket.removalTick + chunkManagerTickOffset) ++ ); ++ } ++ into.tickets.put(entry.getLongKey(), newTickets); ++ } ++ for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry entry = iterator.next(); ++ into.removeTickToChunkExpireTicketCount.merge( ++ (long)(entry.getLongKey() + chunkManagerTickOffset), entry.getValue(), ++ (final Long2IntOpenHashMap t, final Long2IntOpenHashMap f) -> { ++ for (final Iterator itr = f.long2IntEntrySet().fastIterator(); itr.hasNext();) { ++ final Long2IntMap.Entry e = itr.next(); ++ t.addTo(e.getLongKey(), e.getIntValue()); ++ } ++ return t; ++ } ++ ); ++ } ++ ++ // add them all ++ into.specialCaseUnload.addAll(this.specialCaseUnload); + } + +- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { ++ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.pendingFullLoadUpdate.add(fullLoadUpdate); ++ } // else: fullLoadUpdate is an unloaded chunk holder ++ } + +- if (saveTickCompare != 0) { +- return saveTickCompare; ++ for (final NewChunkHolder autoSave : this.autoSaveQueue) { ++ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.autoSaveQueue.add(autoSave); ++ } // else: autoSave is an unloaded chunk holder ++ } ++ for (final HolderManagerRegionData data : dataSet) { ++ data.currentTick = this.currentTick; ++ } ++ for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = iterator.next(); ++ final long chunkKey = entry.getLongKey(); ++ final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift; ++ final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift; ++ ++ // can never be null, since a chunk holder exists if the ticket set is not empty ++ regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)).tickets.put(chunkKey, entry.getValue()); ++ } ++ for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry entry = iterator.next(); ++ final long tick = entry.getLongKey(); ++ final Long2IntOpenHashMap chunkToCount = entry.getValue(); ++ ++ for (final Iterator itr = chunkToCount.long2IntEntrySet().fastIterator(); itr.hasNext();) { ++ final Long2IntMap.Entry e = itr.next(); ++ final long chunkKey = e.getLongKey(); ++ final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift; ++ final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift; ++ final int count = e.getIntValue(); ++ ++ // can never be null, since a chunk holder exists if the ticket set is not empty ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ ++ data.removeTickToChunkExpireTicketCount.computeIfAbsent(tick, (final long keyInMap) -> { ++ return new Long2IntOpenHashMap(); ++ }).put(chunkKey, count); ++ } ++ } ++ ++ for (final NewChunkHolder special : this.specialCaseUnload) { ++ final int regionCoordinateX = CoordinateUtils.getChunkX(special.chunkX) >> chunkToRegionShift; ++ final int regionCoordinateZ = CoordinateUtils.getChunkZ(special.chunkZ) >> chunkToRegionShift; ++ ++ // can never be null, since this chunk holder is loaded ++ regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)).specialCaseUnload.add(special); ++ } + } ++ } + +- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); +- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); + +- if (coord1 == coord2) { +- throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ if (region == null) { ++ return null; + } + +- return Long.compare(coord1, coord2); +- }); ++ if (this.world != null && this.world != region.getData().world) { ++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); ++ } ++ ++ return region.getData().getHolderManagerRegionData(); ++ } ++ ++ // MUST hold ticket lock ++ private ChunkHolderManager.HolderManagerRegionData getDataFor(final long key) { ++ return this.getDataFor(CoordinateUtils.getChunkX(key), CoordinateUtils.getChunkZ(key)); ++ } ++ ++ // MUST hold ticket lock ++ private ChunkHolderManager.HolderManagerRegionData getDataFor(final int chunkX, final int chunkZ) { ++ if (!this.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Must hold ticket level lock"); ++ } ++ ++ final ThreadedRegioniser.ThreadedRegion region ++ = this.world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); ++ ++ if (region == null) { ++ return null; ++ } ++ ++ return region.getData().getHolderManagerRegionData(); ++ } ++ // Paper end - region threading ++ + + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + this.world = world; +@@ -129,8 +296,13 @@ public final class ChunkHolderManager { + } + + public void close(final boolean save, final boolean halt) { ++ // Paper start - region threading ++ this.close(save, halt, true, true, true); ++ } ++ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) { ++ // Paper end - region threading + TickThread.ensureTickThread("Closing world off-main"); +- if (halt) { ++ if (first && halt) { // Paper - region threading + LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); + if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'"); +@@ -140,9 +312,10 @@ public final class ChunkHolderManager { + } + + if (save) { +- this.saveAllChunks(true, true, true); ++ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Paper - region threading + } + ++ if (last) { // Paper - region threading + if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) { + RegionFileIOThread.flush(); + } +@@ -163,27 +336,34 @@ public final class ChunkHolderManager { + } catch (final IOException ex) { + LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); + } ++ } // Paper - region threading + } + + void ensureInAutosave(final NewChunkHolder holder) { +- if (!this.autoSaveQueue.contains(holder)) { +- holder.lastAutoSave = MinecraftServer.currentTick; +- this.autoSaveQueue.add(holder); ++ // Paper start - region threading ++ final HolderManagerRegionData regionData = this.getCurrentRegionData(); ++ if (!regionData.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = RegionisedServer.getCurrentTick(); ++ // Paper end - region threading ++ regionData.autoSaveQueue.add(holder); + } + } + + public void autoSave() { + final List reschedule = new ArrayList<>(); +- final long currentTick = MinecraftServer.currentTickLong; ++ final long currentTick = RegionisedServer.getCurrentTick(); + final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value(); +- for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { +- final NewChunkHolder holder = this.autoSaveQueue.first(); ++ // Paper start - region threading ++ final HolderManagerRegionData regionData = this.getCurrentRegionData(); ++ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !regionData.autoSaveQueue.isEmpty();) { ++ // Paper end - region threading ++ final NewChunkHolder holder = regionData.autoSaveQueue.first(); + + if (holder.lastAutoSave > maxSaveTime) { + break; + } + +- this.autoSaveQueue.remove(holder); ++ regionData.autoSaveQueue.remove(holder); + + holder.lastAutoSave = currentTick; + if (holder.save(false, false) != null) { +@@ -197,15 +377,20 @@ public final class ChunkHolderManager { + + for (final NewChunkHolder holder : reschedule) { + if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { +- this.autoSaveQueue.add(holder); ++ regionData.autoSaveQueue.add(holder); + } + } + } + + public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { ++ // Paper start - region threading ++ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true); ++ } ++ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) { ++ // Paper end - region threading + final List holders = this.getChunkHolders(); + +- if (logProgress) { ++ if (first && logProgress) { // Paper - region threading + LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'"); + } + +@@ -213,7 +398,7 @@ public final class ChunkHolderManager { + + int saved = 0; + +- long start = System.nanoTime(); ++ final long start = System.nanoTime(); + long lastLog = start; + boolean needsFlush = false; + final int flushInterval = 50; +@@ -224,6 +409,12 @@ public final class ChunkHolderManager { + + for (int i = 0, len = holders.size(); i < len; ++i) { + final NewChunkHolder holder = holders.get(i); ++ // Paper start - region threading ++ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { ++ // skip holders that would fail the thread check ++ continue; ++ } ++ // Paper end - region threading + try { + final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false); + if (saveStat != null) { +@@ -256,7 +447,7 @@ public final class ChunkHolderManager { + } + } + } +- if (flush) { ++ if (last && flush) { // Paper - region threading + RegionFileIOThread.flush(); + } + if (logProgress) { +@@ -290,18 +481,16 @@ public final class ChunkHolderManager { + } + + public boolean hasTickets() { +- this.ticketLock.lock(); +- try { +- return !this.tickets.isEmpty(); +- } finally { +- this.ticketLock.unlock(); +- } ++ return !this.getTicketsCopy().isEmpty(); // Paper - region threading + } + + public String getTicketDebugString(final long coordinate) { + this.ticketLock.lock(); + try { +- final SortedArraySet> tickets = this.tickets.get(coordinate); ++ // Paper start - region threading ++ final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate); ++ final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate); ++ // Paper end - region threading + + return tickets != null ? tickets.first().toString() : "no_ticket"; + } finally { +@@ -312,7 +501,17 @@ public final class ChunkHolderManager { + public Long2ObjectOpenHashMap>> getTicketsCopy() { + this.ticketLock.lock(); + try { +- return this.tickets.clone(); ++ // Paper start - region threading ++ Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); ++ this.world.regioniser.computeForAllRegions((region) -> { ++ for (final LongIterator iterator = region.getData().getHolderManagerRegionData().tickets.keySet().longIterator(); iterator.hasNext();) { ++ final long chunk = iterator.nextLong(); ++ ++ ret.put(chunk, region.getData().getHolderManagerRegionData().tickets.get(chunk)); ++ } ++ }); ++ return ret; ++ // Paper end - region threading + } finally { + this.ticketLock.unlock(); + } +@@ -322,7 +521,11 @@ public final class ChunkHolderManager { + ImmutableList.Builder ret; + this.ticketLock.lock(); + try { +- SortedArraySet> tickets = this.tickets.get(ChunkPos.asLong(x, z)); ++ // Paper start - region threading ++ final long coordinate = CoordinateUtils.getChunkKey(x, z); ++ final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate); ++ final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate); ++ // Paper end - region threading + + if (tickets == null) { + return Collections.emptyList(); +@@ -377,10 +580,27 @@ public final class ChunkHolderManager { + + this.ticketLock.lock(); + try { +- final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : this.currentTick + removeDelay; ++ // Paper start - region threading ++ NewChunkHolder holder = this.chunkHolders.get(chunk); ++ final boolean addToSpecial = holder == null; ++ if (addToSpecial) { ++ // we need to guarantee that a chunk holder exists for each ticket ++ // this must be executed before retrieving the holder manager data for a target chunk, to ensure the ++ // region will exist ++ this.chunkHolders.put(chunk, holder = this.createChunkHolder(chunk)); ++ } ++ ++ final ChunkHolderManager.HolderManagerRegionData targetData = this.getDataFor(chunk); ++ if (addToSpecial) { ++ // no guarantee checkUnload is called for this chunk holder - by adding to the special case unload, ++ // the unload chunks call will perform it ++ targetData.specialCaseUnload.add(holder); ++ } ++ // Paper end - region threading ++ final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : targetData.currentTick + removeDelay; // Paper - region threading + final Ticket ticket = new Ticket<>(type, level, identifier, removeTick); + +- final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { ++ final SortedArraySet> ticketsAtChunk = targetData.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { // Paper - region threading + return SortedArraySet.create(4); + }); + +@@ -392,25 +612,25 @@ public final class ChunkHolderManager { + final long oldRemovalTick = current.removalTick; + if (removeTick != oldRemovalTick) { + if (oldRemovalTick != NO_TIMEOUT_MARKER) { +- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(oldRemovalTick); ++ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(oldRemovalTick); // Paper - region threading + final int prevCount = removeCounts.addTo(chunk, -1); + + if (prevCount == 1) { + removeCounts.remove(chunk); + if (removeCounts.isEmpty()) { +- this.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); ++ targetData.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); // Paper - region threading + } + } + } + if (removeTick != NO_TIMEOUT_MARKER) { +- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { ++ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Paper - region threading + return new Long2IntOpenHashMap(); + }).addTo(chunk, 1); + } + } + } else { + if (removeTick != NO_TIMEOUT_MARKER) { +- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { ++ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Paper - region threading + return new Long2IntOpenHashMap(); + }).addTo(chunk, 1); + } +@@ -439,35 +659,43 @@ public final class ChunkHolderManager { + return false; + } + ++ final ChunkHolderManager.HolderManagerRegionData currRegionData = this.getCurrentRegionData(); // Paper - region threading ++ + this.ticketLock.lock(); + try { +- final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); ++ // Paper start - region threading ++ final ChunkHolderManager.HolderManagerRegionData targetData = this.getDataFor(chunk); ++ ++ final boolean sameRegion = currRegionData == targetData; ++ ++ final SortedArraySet> ticketsAtChunk = targetData == null ? null : targetData.tickets.get(chunk); ++ // Paper end - region threading + if (ticketsAtChunk == null) { + return false; + } + + final int oldLevel = getTicketLevelAt(ticketsAtChunk); +- final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, -2L)); ++ final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, PROBE_MARKER)); // Paper - region threading + + if (ticket == null) { + return false; + } + + if (ticketsAtChunk.isEmpty()) { +- this.tickets.remove(chunk); ++ targetData.tickets.remove(chunk); // Paper - region threading + } + + final int newLevel = getTicketLevelAt(ticketsAtChunk); + + final long removeTick = ticket.removalTick; + if (removeTick != NO_TIMEOUT_MARKER) { +- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(removeTick); ++ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(removeTick); // Paper - region threading + final int currCount = removeCounts.addTo(chunk, -1); + + if (currCount == 1) { + removeCounts.remove(chunk); + if (removeCounts.isEmpty()) { +- this.removeTickToChunkExpireTicketCount.remove(removeTick); ++ targetData.removeTickToChunkExpireTicketCount.remove(removeTick); // Paper - region threading + } + } + } +@@ -476,6 +704,13 @@ public final class ChunkHolderManager { + this.updateTicketLevel(chunk, newLevel); + } + ++ // Paper start - region threading ++ // if we're not the target region, we should not change the ticket levels while the target region may be ticking ++ if (!sameRegion && newLevel > level) { ++ this.addTicketAtLevel(TicketType.UNKNOWN, chunk, level, new ChunkPos(chunk)); ++ } ++ // Paper end - region threading ++ + return true; + } finally { + this.ticketLock.unlock(); +@@ -516,24 +751,33 @@ public final class ChunkHolderManager { + + this.ticketLock.lock(); + try { +- for (final LongIterator iterator = new LongArrayList(this.tickets.keySet()).longIterator(); iterator.hasNext();) { +- final long chunk = iterator.nextLong(); ++ // Paper start - region threading ++ this.world.regioniser.computeForAllRegions((region) -> { ++ for (final LongIterator iterator = new LongArrayList(region.getData().getHolderManagerRegionData().tickets.keySet()).longIterator(); iterator.hasNext();) { ++ final long chunk = iterator.nextLong(); + +- this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier); +- } ++ this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier); ++ } ++ }); ++ // Paper end - region threading + } finally { + this.ticketLock.unlock(); + } + } + + public void tick() { +- TickThread.ensureTickThread("Cannot tick ticket manager off-main"); ++ // Paper start - region threading ++ final ChunkHolderManager.HolderManagerRegionData data = this.getCurrentRegionData(); ++ if (data == null) { ++ throw new IllegalStateException("Not running tick() while on a region"); ++ } ++ // Paper end - region threading + + this.ticketLock.lock(); + try { +- final long tick = ++this.currentTick; ++ final long tick = ++data.currentTick; // Paper - region threading + +- final Long2IntOpenHashMap toRemove = this.removeTickToChunkExpireTicketCount.remove(tick); ++ final Long2IntOpenHashMap toRemove = data.removeTickToChunkExpireTicketCount.remove(tick); // Paper - region threading + + if (toRemove == null) { + return; +@@ -546,10 +790,10 @@ public final class ChunkHolderManager { + for (final LongIterator iterator = toRemove.keySet().longIterator(); iterator.hasNext();) { + final long chunk = iterator.nextLong(); + +- final SortedArraySet> tickets = this.tickets.get(chunk); ++ final SortedArraySet> tickets = data.tickets.get(chunk); // Paper - region threading + tickets.removeIf(expireNow); + if (tickets.isEmpty()) { +- this.tickets.remove(chunk); ++ data.tickets.remove(chunk); // Paper - region threading + this.ticketLevelPropagator.removeSource(chunk); + } else { + this.ticketLevelPropagator.setSource(chunk, convertBetweenTicketLevels(tickets.first().getTicketLevel())); +@@ -798,30 +1042,62 @@ public final class ChunkHolderManager { + if (changedFullStatus.isEmpty()) { + return; + } +- if (!TickThread.isTickThread()) { +- this.taskScheduler.scheduleChunkTask(() -> { +- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); +- } + +- ChunkHolderManager.this.processPendingFullUpdate(); +- }, PrioritisedExecutor.Priority.HIGHEST); +- } else { +- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>(); ++ final List thisRegionHolders = new ArrayList<>(); ++ ++ final int regionShift = this.world.regioniser.sectionChunkShift; ++ final ThreadedRegioniser.ThreadedRegion thisRegion ++ = TickRegionScheduler.getCurrentRegion(); ++ ++ for (final NewChunkHolder holder : changedFullStatus) { ++ final int regionX = holder.chunkX >> regionShift; ++ final int regionZ = holder.chunkZ >> regionShift; ++ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ); ++ ++ // region may be null ++ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) { ++ thisRegionHolders.add(holder); ++ } else { ++ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(holder); ++ } ++ } ++ ++ if (!thisRegionHolders.isEmpty()) { ++ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders); ++ } ++ ++ if (!sectionToUpdates.isEmpty()) { ++ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ ++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift; ++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; ++ ++ final List regionHolders = entry.getValue(); ++ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { // Paper - region threading ++ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); ++ ChunkHolderManager.this.processPendingFullUpdate(); ++ }, PrioritisedExecutor.Priority.HIGHEST); + } + } + } + + final ReferenceLinkedOpenHashSet unloadQueue = new ReferenceLinkedOpenHashSet<>(); + ++ /* ++ * Note: Only called on chunk holders that the current ticking region owns ++ */ + private void removeChunkHolder(final NewChunkHolder holder) { + holder.killed = true; + holder.vanillaChunkHolder.onChunkRemove(); +- this.autoSaveQueue.remove(holder); ++ // Paper - region threading + ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); ++ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Paper - region threading + this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); + } + +@@ -839,23 +1115,42 @@ public final class ChunkHolderManager { + throw new IllegalStateException("Cannot hold scheduling lock while calling processUnloads"); + } + ++ final ChunkHolderManager.HolderManagerRegionData currentData = this.getCurrentRegionData(); // Paper - region threading ++ + final List unloadQueue; + final List scheduleList = new ArrayList<>(); + this.ticketLock.lock(); + try { + this.taskScheduler.schedulingLock.lock(); + try { ++ // Paper start - region threading ++ for (final NewChunkHolder special : currentData.specialCaseUnload) { ++ special.checkUnload(); ++ } ++ currentData.specialCaseUnload.clear(); ++ // Paper end - region threading + if (this.unloadQueue.isEmpty()) { + return; + } + // in order to ensure all chunks in the unload queue do not have a pending ticket level update, + // process them now + this.processTicketUpdates(false, false, scheduleList); +- unloadQueue = new ArrayList<>((int)(this.unloadQueue.size() * 0.05) + 1); + +- final int unloadCount = Math.max(50, (int)(this.unloadQueue.size() * 0.05)); +- for (int i = 0; i < unloadCount && !this.unloadQueue.isEmpty(); ++i) { +- final NewChunkHolder chunkHolder = this.unloadQueue.removeFirst(); ++ // Paper start - region threading ++ final ArrayDeque toUnload = new ArrayDeque<>(); ++ // The unload queue is globally maintained, but we can only unload chunks in our region ++ for (final NewChunkHolder holder : this.unloadQueue) { ++ if (TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { ++ toUnload.add(holder); ++ } ++ } ++ // Paper end - region threading ++ ++ final int unloadCount = Math.max(50, (int)(toUnload.size() * 0.05)); // Paper - region threading ++ unloadQueue = new ArrayList<>(unloadCount + 1); // Paper - region threading ++ for (int i = 0; i < unloadCount && !toUnload.isEmpty(); ++i) { // Paper - region threading ++ final NewChunkHolder chunkHolder = toUnload.removeFirst(); // Paper - region threading ++ this.unloadQueue.remove(chunkHolder); // Paper - region threading + if (chunkHolder.isSafeToUnload() != null) { + LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); + continue; +@@ -1193,7 +1488,12 @@ public final class ChunkHolderManager { + + // only call on tick thread + protected final boolean processPendingFullUpdate() { +- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ final HolderManagerRegionData data = this.getCurrentRegionData(); ++ if (data == null) { ++ return false; ++ } ++ ++ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate; + + boolean ret = false; + +@@ -1204,9 +1504,7 @@ public final class ChunkHolderManager { + ret |= holder.handleFullStatusChange(changedFullStatus); + + if (!changedFullStatus.isEmpty()) { +- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +- pendingFullLoadUpdate.add(changedFullStatus.get(i)); +- } ++ this.addChangedStatuses(changedFullStatus); + changedFullStatus.clear(); + } + } +@@ -1256,7 +1554,7 @@ public final class ChunkHolderManager { + + private JsonObject getDebugJsonNoLock() { + final JsonObject ret = new JsonObject(); +- ret.addProperty("current_tick", Long.valueOf(this.currentTick)); ++ // Paper - region threading - move down + + final JsonArray unloadQueue = new JsonArray(); + ret.add("unload_queue", unloadQueue); +@@ -1275,60 +1573,73 @@ public final class ChunkHolderManager { + holders.add(holder.getDebugJson()); + } + +- final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); +- ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); ++ // Paper start - region threading ++ final JsonArray regions = new JsonArray(); ++ ret.add("regions", regions); ++ this.world.regioniser.computeForAllRegionsUnsynchronised((region) -> { ++ final JsonObject regionJson = new JsonObject(); ++ regions.add(regionJson); ++ ++ final TickRegions.TickRegionData regionData = region.getData(); + +- for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { +- final long tick = tickEntry.getLongKey(); +- final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); ++ regionJson.addProperty("current_tick", Long.valueOf(regionData.getCurrentTick())); + +- final JsonObject tickJson = new JsonObject(); +- removeTickToChunkExpireTicketCount.add(tickJson); ++ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); ++ regionJson.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); + +- tickJson.addProperty("tick", Long.valueOf(tick)); ++ for (final Long2ObjectMap.Entry tickEntry : regionData.getHolderManagerRegionData().removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { ++ final long tick = tickEntry.getLongKey(); ++ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); + +- final JsonArray tickEntries = new JsonArray(); +- tickJson.add("entries", tickEntries); ++ final JsonObject tickJson = new JsonObject(); ++ removeTickToChunkExpireTicketCount.add(tickJson); + +- for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { +- final long coordinate = entry.getLongKey(); +- final int count = entry.getIntValue(); ++ tickJson.addProperty("tick", Long.valueOf(tick)); + +- final JsonObject entryJson = new JsonObject(); +- tickEntries.add(entryJson); ++ final JsonArray tickEntries = new JsonArray(); ++ tickJson.add("entries", tickEntries); + +- entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); +- entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); +- entryJson.addProperty("count", Integer.valueOf(count)); ++ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { ++ final long coordinate = entry.getLongKey(); ++ final int count = entry.getIntValue(); ++ ++ final JsonObject entryJson = new JsonObject(); ++ tickEntries.add(entryJson); ++ ++ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ entryJson.addProperty("count", Integer.valueOf(count)); ++ } + } +- } + +- final JsonArray allTicketsJson = new JsonArray(); +- ret.add("tickets", allTicketsJson); ++ final JsonArray allTicketsJson = new JsonArray(); ++ regionJson.add("tickets", allTicketsJson); + +- for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { +- final long coordinate = coordinateTickets.getLongKey(); +- final SortedArraySet> tickets = coordinateTickets.getValue(); ++ for (final Long2ObjectMap.Entry>> coordinateTickets : regionData.getHolderManagerRegionData().tickets.long2ObjectEntrySet()) { ++ final long coordinate = coordinateTickets.getLongKey(); ++ final SortedArraySet> tickets = coordinateTickets.getValue(); + +- final JsonObject coordinateJson = new JsonObject(); +- allTicketsJson.add(coordinateJson); ++ final JsonObject coordinateJson = new JsonObject(); ++ allTicketsJson.add(coordinateJson); + +- coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); +- coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + +- final JsonArray ticketsSerialized = new JsonArray(); +- coordinateJson.add("tickets", ticketsSerialized); ++ final JsonArray ticketsSerialized = new JsonArray(); ++ coordinateJson.add("tickets", ticketsSerialized); + +- for (final Ticket ticket : tickets) { +- final JsonObject ticketSerialized = new JsonObject(); +- ticketsSerialized.add(ticketSerialized); ++ for (final Ticket ticket : tickets) { ++ final JsonObject ticketSerialized = new JsonObject(); ++ ticketsSerialized.add(ticketSerialized); + +- ticketSerialized.addProperty("type", ticket.getType().toString()); +- ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); +- ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); +- ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); ++ ticketSerialized.addProperty("type", ticket.getType().toString()); ++ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); ++ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); ++ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); ++ } + } +- } ++ }); ++ // Paper end - region threading + + return ret; + } +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..b3ae296cdf3f81550e2cc4d9516f4ed928760a81 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +@@ -113,7 +113,7 @@ public final class ChunkTaskScheduler { + public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; + public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; + +- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); ++ // Paper - regionised ticking + + final ReentrantLock schedulingLock = new ReentrantLock(); + public final ChunkHolderManager chunkHolderManager; +@@ -240,14 +240,13 @@ public final class ChunkTaskScheduler { + }; + + // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions +- this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); ++ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); // Paper - region threading + // so, make the main thread pick it up + MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); + } + + public boolean executeMainThreadTask() { +- TickThread.ensureTickThread("Cannot execute main thread task off-main"); +- return this.mainThreadExecutor.executeTask(); ++ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Paper - regionised ticking + } + + public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { +@@ -267,7 +266,7 @@ public final class ChunkTaskScheduler { + public void scheduleTickingState(final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); +@@ -380,9 +379,50 @@ public final class ChunkTaskScheduler { + }); + } + ++ // Paper start - region threading ++ // only appropriate to use with ServerLevel#syncLoadNonFull ++ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final PrioritisedExecutor.Priority priority) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); ++ final List tasks = new ArrayList<>(); ++ this.chunkHolderManager.ticketLock.lock(); ++ try { ++ this.schedulingLock.lock(); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ return false; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ return true; ++ } else { ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ } ++ } ++ } finally { ++ this.schedulingLock.unlock(); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLock.unlock(); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ return true; ++ } ++ // Paper end - region threading ++ + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); +@@ -409,7 +449,7 @@ public final class ChunkTaskScheduler { + this.chunkHolderManager.processTicketUpdates(); + } + +- final Consumer loadCallback = (final ChunkAccess chunk) -> { ++ final Consumer loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); +@@ -449,7 +489,9 @@ public final class ChunkTaskScheduler { + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } +- chunkHolder.addStatusConsumer(toStatus, loadCallback); ++ if (loadCallback != null) { ++ chunkHolder.addStatusConsumer(toStatus, loadCallback); ++ } + } + } + } finally { +@@ -463,7 +505,7 @@ public final class ChunkTaskScheduler { + tasks.get(i).schedule(); + } + +- if (!scheduled) { ++ if (loadCallback != null && !scheduled) { + // couldn't schedule + try { + loadCallback.accept(chunk); +@@ -652,7 +694,7 @@ public final class ChunkTaskScheduler { + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { +- return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); ++ throw new UnsupportedOperationException(); // Paper - regionised ticking + } + + /** +@@ -660,7 +702,7 @@ public final class ChunkTaskScheduler { + */ + @Deprecated + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { +- return this.mainThreadExecutor.queueRunnable(run, priority); ++ throw new UnsupportedOperationException(); // Paper - regionised ticking + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +@@ -669,28 +711,33 @@ public final class ChunkTaskScheduler { + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { +- return this.mainThreadExecutor.createTask(run, priority); ++ return MinecraftServer.getServer().regionisedServer.taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Paper - regionised ticking + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { +- return this.mainThreadExecutor.queueRunnable(run); ++ return this.scheduleChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); // TODO rebase into chunk system patch + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { +- return this.mainThreadExecutor.queueRunnable(run, priority); ++ return MinecraftServer.getServer().regionisedServer.taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Paper - regionised ticking + } + +- public void executeTasksUntil(final BooleanSupplier exit) { +- if (Bukkit.isPrimaryThread()) { +- this.mainThreadExecutor.executeConditionally(exit); +- } else { +- long counter = 1L; +- while (!exit.getAsBoolean()) { +- counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms +- } +- } ++ // Paper start - region threading ++ // this function is guaranteed to never touch the ticket lock or schedule lock ++ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the ++ // ticket lock in the schedule logic ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); ++ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { ++ MinecraftServer.getServer().regionisedServer.taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority); ++ }); ++ return ret; + } ++ // Paper end - region threading ++ ++ // Paper - regionised ticking + + public boolean halt(final boolean sync, final long maxWaitNS) { + this.lightExecutor.halt(); +@@ -699,6 +746,7 @@ public final class ChunkTaskScheduler { + this.loadExecutor.halt(); + final long time = System.nanoTime(); + if (sync) { ++ // start at 10 * 0.5ms -> 5ms + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + if ( + !this.lightExecutor.isActive() && +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +index 8013dd333e27aa5fd0beb431fa32491eec9f5246..63d2cec73e2bcf7031cdb5dfca8151f067860ec0 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -708,7 +708,7 @@ public final class NewChunkHolder { + boolean killed; + + // must hold scheduling lock +- private void checkUnload() { ++ void checkUnload() { // Paper - region threading + if (this.killed) { + return; + } +@@ -1412,7 +1412,7 @@ public final class NewChunkHolder { + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid +- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Paper - region threading + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); +@@ -1455,7 +1455,7 @@ public final class NewChunkHolder { + } + + // must be scheduled to main, we do not trust the callback to not do anything stupid +- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Paper - region threading + for (final Consumer consumer : consumers) { + try { + consumer.accept(chunk); +@@ -1715,7 +1715,7 @@ public final class NewChunkHolder { + return this.entityChunk; + } + +- public long lastAutoSave; ++ public long lastAutoSave; // Paper - region threaded - change to relative delay + + public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} + +@@ -1865,7 +1865,7 @@ public final class NewChunkHolder { + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr) { +- LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase + if (unloading && !completing) { + this.completeAsyncChunkDataSave(null); + } +@@ -1913,7 +1913,7 @@ public final class NewChunkHolder { + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr) { +- LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase + } + + return true; +@@ -1939,7 +1939,7 @@ public final class NewChunkHolder { + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr) { +- LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase + } + + return true; +diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java +index d31b5ed47cffc61c90c926a0cd2005b72ebddfc5..e22632ba0d84c796a9bab3a1a9c43d5e5dcf73e8 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommands.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java +@@ -17,7 +17,8 @@ public final class PaperCommands { + private static final Map COMMANDS = new HashMap<>(); + static { + COMMANDS.put("paper", new PaperCommand("paper")); +- COMMANDS.put("mspt", new MSPTCommand("mspt")); ++ COMMANDS.put("tpa", new io.papermc.paper.threadedregions.commands.CommandsTPA()); // Paper - region threading ++ COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Paper - region threading + } + + public static void registerCommands(final MinecraftServer server) { +diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +index cd2e4d792e972b8bf1e07b8961594a670ae949cf..645fe0d8d6d301fcb7897f9162fac8971bc6abbe 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +@@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; + public final class HeapDumpCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + this.dumpHeap(sender); ++ }); // Paper - region threading + return true; + } + +diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..872b59bccddf2adc7bce4bec5ecdcf5f0a0f0815 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +@@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED; + public final class ReloadCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + this.doReload(sender); ++ }); // Paper - region threading + return true; + } + +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 9f5f0d8ddc8f480b48079c70e38c9c08eff403f6..1d0e1311de8dafd42070edea40b1990c04eb7746 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -288,6 +288,18 @@ public class GlobalConfiguration extends ConfigurationPart { + public boolean strictAdvancementDimensionCheck = false; + } + ++ // Paper start - threaded regions ++ public ThreadedRegions threadedRegions; ++ public class ThreadedRegions extends Post { ++ ++ public int threads = -1; ++ ++ @Override ++ public void postProcess() { ++ io.papermc.paper.threadedregions.TickRegions.init(this); ++ } ++ } ++ // Paper end - threaded regions + public ChunkLoadingBasic chunkLoadingBasic; + + public class ChunkLoadingBasic extends ConfigurationPart { +diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d9687722e02dfd4088c7030abbf5008eb0a092c8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +@@ -0,0 +1,181 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.Validate; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import net.minecraft.world.entity.Entity; ++import org.bukkit.craftbukkit.entity.CraftEntity; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Consumer; ++ ++/** ++ * An entity can move between worlds with an arbitrary tick delay, be temporarily removed ++ * for players (i.e end credits), be partially removed from world state (i.e inactive but not removed), ++ * teleport between ticking regions, teleport between worlds (which will change the underlying Entity object ++ * for non-players), and even be removed entirely from the server. The uncertainty of an entity's state can make ++ * it difficult to schedule tasks without worrying about undefined behaviors resulting from any of the states listed ++ * previously. ++ * ++ *

++ * This class is designed to eliminate those states by providing an interface to run tasks only when an entity ++ * is contained in a world, on the owning thread for the region, and by providing the current Entity object. ++ * The scheduler also allows a task to provide a callback, the "retired" callback, that will be invoked ++ * if the entity is removed before a task that was scheduled could be executed. The scheduler is also ++ * completely thread-safe, allowing tasks to be scheduled from any thread context. The scheduler also indicates ++ * properly whether a task was scheduled successfully (i.e scheduler not retired), thus the code scheduling any task ++ * knows whether the given callbacks will be invoked eventually or not - which may be critical for off-thread ++ * contexts. ++ *

++ */ ++public final class EntityScheduler { ++ ++ /** ++ * The Entity. Note that it is the CraftEntity, since only that class properly tracks world transfers. ++ */ ++ public final CraftEntity entity; ++ ++ private static final record ScheduledTask(Consumer run, Consumer retired) {} ++ ++ private long tickCount = 0L; ++ private static final long RETIRED_TICK_COUNT = -1L; ++ private final Object stateLock = new Object(); ++ private final Long2ObjectOpenHashMap> oneTimeDelayed = new Long2ObjectOpenHashMap<>(); ++ ++ private final ArrayDeque currentlyExecuting = new ArrayDeque<>(); ++ ++ public EntityScheduler(final CraftEntity entity) { ++ this.entity = Validate.notNull(entity); ++ } ++ ++ /** ++ * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback ++ * on all currently scheduled tasks. ++ * ++ *

++ * Note: This should only be invoked after synchronously removing the entity from the world. ++ *

++ * ++ * @throws IllegalStateException If the scheduler is already retired. ++ */ ++ public void retire() { ++ synchronized (this.stateLock) { ++ if (this.tickCount == RETIRED_TICK_COUNT) { ++ throw new IllegalStateException("Already retired"); ++ } ++ this.tickCount = RETIRED_TICK_COUNT; ++ } ++ ++ final Entity thisEntity = this.entity.getHandle(); ++ ++ // correctly handle and order retiring while running executeTick ++ for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { ++ final ScheduledTask task = this.currentlyExecuting.pollFirst(); ++ final Consumer retireTask = (Consumer)task.retired; ++ if (retireTask == null) { ++ continue; ++ } ++ ++ retireTask.accept(thisEntity); ++ } ++ ++ for (final List tasks : this.oneTimeDelayed.values()) { ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ final ScheduledTask task = tasks.get(i); ++ final Consumer retireTask = (Consumer)task.retired; ++ if (retireTask == null) { ++ continue; ++ } ++ ++ retireTask.accept(thisEntity); ++ } ++ } ++ } ++ ++ /** ++ * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity ++ * removed), then returns {@code false}. Otherwise, either the run callback will be invoked after the specified delay, ++ * or the retired callback will be invoked if the scheduler is retired. ++ * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove ++ * other entities, load chunks, load worlds, modify ticket levels, etc. ++ * ++ *

++ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity. ++ *

++ *

++ * The run and retired callback take an Entity parameter representing the current object entity that the scheduler ++ * is tied to. Since the scheduler is transferred when an entity changes dimensions, it is possible the entity parameter ++ * is not the same when the task was first scheduled. Thus, only the parameter provided should be used. ++ *

++ * @param run The callback to run after the specified delay, may not be null. ++ * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. ++ * @param delay The delay in ticks before the run callback is invoked. Any value less-than 1 is treated as 1. ++ * @return {@code true} if the task was scheduled, which means that either the run function or the retired function ++ * will be invoked (but never both), or {@code false} indicating neither the run nor retired function will be invoked ++ * since the scheduler has been retired. ++ */ ++ public boolean schedule(final Consumer run, final Consumer retired, final long delay) { ++ Validate.notNull(run, "Run task may not be null"); ++ ++ final ScheduledTask task = new ScheduledTask(run, retired); ++ synchronized (this.stateLock) { ++ if (this.tickCount == RETIRED_TICK_COUNT) { ++ return false; ++ } ++ this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(task); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Executes a tick for the scheduler. ++ * ++ * @throws IllegalStateException If the scheduler is retired. ++ */ ++ public void executeTick() { ++ final Entity thisEntity = this.entity.getHandle(); ++ ++ TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously"); ++ final List toRun; ++ synchronized (this.stateLock) { ++ if (this.tickCount == RETIRED_TICK_COUNT) { ++ throw new IllegalStateException("Ticking retired scheduler"); ++ } ++ ++this.tickCount; ++ if (this.oneTimeDelayed.isEmpty()) { ++ toRun = null; ++ } else { ++ toRun = this.oneTimeDelayed.remove(this.tickCount); ++ } ++ } ++ ++ if (toRun != null) { ++ for (int i = 0, len = toRun.size(); i < len; ++i) { ++ this.currentlyExecuting.addLast(toRun.get(i)); ++ } ++ } ++ ++ // Note: It is allowed for the tasks executed to retire the entity in a given task. ++ for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { ++ if (!TickThread.isTickThreadFor(thisEntity)) { ++ // tp has been queued sync by one of the tasks ++ // in this case, we need to delay the tasks for next tick ++ break; ++ } ++ final ScheduledTask task = this.currentlyExecuting.pollFirst(); ++ ++ if (this.tickCount != RETIRED_TICK_COUNT) { ++ ((Consumer)task.run).accept(thisEntity); ++ } else { ++ // retired synchronously ++ // note: here task is null ++ break; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..70c3accbab4e69268435c6f4fb13d29c7662283d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java +@@ -0,0 +1,112 @@ ++package io.papermc.paper.threadedregions; ++ ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.TickThread; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.TimeUnit; ++ ++public final class RegionShutdownThread extends TickThread { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ ThreadedRegioniser.ThreadedRegion shuttingDown; ++ ++ public RegionShutdownThread(final String name) { ++ super(name); ++ this.setUncaughtExceptionHandler((thread, thr) -> { ++ LOGGER.error("Error shutting down server", thr); ++ }); ++ } ++ ++ static ThreadedRegioniser.ThreadedRegion getRegion() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ return shutdownThread.shuttingDown; ++ } ++ return null; ++ } ++ ++ ++ static RegionisedWorldData getWorldData() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ // no fast path for shutting down ++ if (shutdownThread.shuttingDown != null) { ++ return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); ++ } ++ } ++ return null; ++ } ++ ++ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves ++ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because ++ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. ++ // Thus, the only operation that the shutdown thread will perform ++ ++ private void saveLevelData(final ServerLevel world) { ++ try { ++ world.saveLevelData(); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); ++ } ++ } ++ ++ private void saveRegionChunks(final ThreadedRegioniser.ThreadedRegion region, ++ final boolean first, final boolean last) { ++ final ChunkPos center = region.getCenterChunk(); ++ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); ++ try { ++ this.shuttingDown = region; ++ region.regioniser.world.chunkTaskScheduler.chunkHolderManager.close(true, true, first, last, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ private void haltWorldNoRegions(final ServerLevel world) { ++ try { ++ world.chunkTaskScheduler.chunkHolderManager.close(true, true, true, true, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to close world '" + world.getWorld().getName() + "' with no regions", thr); ++ } ++ } ++ ++ @Override ++ public final void run() { ++ // await scheduler termination ++ LOGGER.info("Awaiting scheduler termination for 60s"); ++ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Scheduler halted"); ++ } else { ++ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); ++ } ++ ++ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ final List> ++ regions = new ArrayList<>(); ++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ final ThreadedRegioniser.ThreadedRegion region = regions.get(i); ++ this.saveRegionChunks(region, i == 0, (i + 1) == len); ++ } ++ ++ if (regions.isEmpty()) { ++ // still need to halt the chunk system ++ this.haltWorldNoRegions(world); ++ } ++ ++ this.saveLevelData(world); ++ } ++ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) ++ // done, part 2 should call exit() ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3549e5f3359f38b207e189d89595442018c9dfa2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedData.java +@@ -0,0 +1,235 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.Validate; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import javax.annotation.Nullable; ++import java.util.function.Supplier; ++ ++/** ++ * Use to manage data that needs to be regionised. ++ *

++ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionisedData} object is GC'd. ++ * The data is held in reference to the world it resides in. ++ *

++ *

++ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. ++ * That is, the data may or may not cross worlds. As such, the {@code RegionisedData} object must be instanced ++ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. ++ * See below for more details on instancing per world. ++ *

++ *

++ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current ++ * region's world does not match the {@code RegionisedData}'s world. Consider the usages of {@code RegionisedData} below ++ * see why the behavior may or may not be desirable: ++ *

++ *         {@code
++ *         public class EntityTickList {
++ *             private final List entities = new ArrayList<>();
++ *
++ *             public void addEntity(Entity e) {
++ *                 this.entities.add(e);
++ *             }
++ *
++ *             public void removeEntity(Entity e) {
++ *                 this.entities.remove(e);
++ *             }
++ *         }
++ *
++ *         public class World {
++ *
++ *             // callback is left out of this example
++ *             // note: world != null here
++ *             public final RegionisedData entityTickLists =
++ *                 new RegionisedData<>(this, () -> new EntityTickList(), ...);
++ *
++ *             public void addTickingEntity(Entity e) {
++ *                 // What we expect here is that this world is the
++ *                 // current ticking region's world.
++ *                 // If that is true, then calling this.entityTickLists.get()
++ *                 // will retrieve the current region's EntityTickList
++ *                 // for this world, which is fine since the current
++ *                 // region is contained within this world.
++ *
++ *                 // But if the current region's world is not this world,
++ *                 // and if the world check is disabled, then we will actually
++ *                 // retrieve _this_ world's EntityTickList for the region,
++ *                 // and NOT the EntityTickList for the region's world.
++ *                 // This is because the RegionisedData object is instantiated
++ *                 // per world.
++ *                 this.entityTickLists.get().addEntity(e);
++ *             }
++ *         }
++ *
++ *         public class TickTimes {
++ *
++ *             private final List tickTimesNS = new ArrayList<>();
++ *
++ *             public void completeTick(long timeNS) {
++ *                 this.tickTimesNS.add(timeNS);
++ *             }
++ *
++ *             public double getAverageTickLengthMS() {
++ *                 double sum = 0.0;
++ *                 for (long time : tickTimesNS) {
++ *                     sum += (double)time;
++ *                 }
++ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
++ *             }
++ *         }
++ *
++ *         public class Server {
++ *             public final List worlds = ...;
++ *
++ *             // callback is left out of this example
++ *             // note: world == null here, because this RegionisedData object
++ *             // is not instantiated per world, but rather globally.
++ *             public final RegionisedData tickTimes =
++ *                  new RegionisedData<>(null, () -> new TickTimes(), ...);
++ *         }
++ *         }
++ *     
++ * In general, it is advised that if a RegionisedData object is instantiated per world, that world checking ++ * is enabled for it by passing the world to the constructor. ++ *

++ */ ++public final class RegionisedData { ++ ++ private final ServerLevel world; ++ private final Supplier initialValueSupplier; ++ private final RegioniserCallback callback; ++ ++ /** ++ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must ++ * never produce {@code null} values. ++ *

++ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking ++ * operations may deadlock the entire server and as such the function should be completely non-blocking ++ * and must complete in a timely manner. ++ *

++ *

++ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever ++ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} ++ * for an entity tick list is invalid since the entities are tied to a world and region, ++ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to ++ * region only. ++ *

++ * @param world The world in which the region data resides. ++ * @param supplier Initial value supplier used to lazy initialise region data. ++ * @param callback Region callback to manage this regionised data. ++ */ ++ public RegionisedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { ++ this.world = world; ++ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); ++ this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); ++ } ++ ++ T createNewValue() { ++ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); ++ } ++ ++ RegioniserCallback getCallback() { ++ return this.callback; ++ } ++ ++ /** ++ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @return the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @throws IllegalStateException If the following are true: The server is in region ticking mode, ++ * this {@code RegionisedData}'s world is not {@code null}, ++ * and the current ticking region's world does not match this {@code RegionisedData}'s world. ++ */ ++ public @Nullable T get() { ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ ++ if (region == null) { ++ return null; ++ } ++ ++ if (this.world != null && this.world != region.getData().world) { ++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); ++ } ++ ++ return region.getData().getOrCreateRegionisedData(this); ++ } ++ ++ /** ++ * Class responsible for handling merge / split requests from the regioniser. ++ *

++ * It is critical to note that each function is called while holding the region lock. ++ *

++ */ ++ public static interface RegioniserCallback { ++ ++ /** ++ * Completely merges the data in {@code from} to {@code into}. ++ *

++ * Calculating Tick Offsets: ++ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines ++ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in ++ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from ++ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for ++ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} ++ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, ++ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and ++ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate ++ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. ++ *

++ *

++ * Critical Notes: ++ *

  • ++ *
      ++ * This function is called while the region lock is held, so any blocking operations may ++ * deadlock the entire server and as such the function should be completely non-blocking and must complete ++ * in a timely manner. ++ *
    ++ *
      ++ * This function may not throw any exceptions, or the server will be left in an unrecoverable state. ++ *
    ++ *
  • ++ *

    ++ * ++ * @param from The data to merge from. ++ * @param into The data to merge into. ++ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. ++ */ ++ public void merge(final T from, final T into, final long fromTickOffset); ++ ++ /** ++ * Splits the data in {@code from} into {@code dataSet}. ++ *

    ++ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. ++ * To convert from chunk coordinates to region coordinates and keys, see the code below: ++ *

    ++         *         {@code
    ++         *         int chunkX = ...;
    ++         *         int chunkZ = ...;
    ++         *
    ++         *         int regionSectionX = chunkX >> chunkToRegionShift;
    ++         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    ++         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    ++         *         }
    ++         *     
    ++ *

    ++ *

    ++ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the ++ * data that is owned by the region which occupies the region section. ++ *

    ++ *

    ++ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because ++ * the new regions formed from the split will start at the same tick number, and so no adjustment is required. ++ *

    ++ * ++ * @param from The data to split from. ++ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. ++ * @param regionToData Lookup hash table from region section key to . ++ * @param dataSet The data set to split into. ++ */ ++ public void split( ++ final T from, final int chunkToRegionShift, ++ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..31209d5cef17f9bdfe03736654d7dcd222afee51 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java +@@ -0,0 +1,355 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import com.mojang.authlib.GameProfile; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.TickThread; ++import net.minecraft.CrashReport; ++import net.minecraft.ReportedException; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketListener; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.chat.Component; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; ++import net.minecraft.network.protocol.status.ServerStatus; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.dedicated.DedicatedServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.server.network.ServerLoginPacketListenerImpl; ++import net.minecraft.server.players.PlayerList; ++import net.minecraft.util.Mth; ++import net.minecraft.world.level.GameRules; ++import net.minecraft.world.level.levelgen.LegacyRandomSource; ++import org.slf4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.List; ++import java.util.concurrent.CopyOnWriteArrayList; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BooleanSupplier; ++ ++public final class RegionisedServer { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final RegionisedServer INSTANCE = new RegionisedServer(); ++ ++ public final RegionisedTaskQueue taskQueue = new RegionisedTaskQueue(); ++ ++ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); ++ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); ++ ++ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); ++ ++ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); ++ ++ public static RegionisedServer getInstance() { ++ return INSTANCE; ++ } ++ ++ public void addConnection(final Connection conn) { ++ this.connections.add(conn); ++ } ++ ++ private boolean removeConnection(final Connection conn) { ++ return this.connections.remove(conn); ++ } ++ ++ public void addWorld(final ServerLevel world) { ++ this.worlds.add(world); ++ } ++ ++ public void init() { ++ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ TickRegions.getScheduler().scheduleRegion(this.tickHandle); ++ TickRegions.getScheduler().init(); ++ } ++ ++ public void invalidateStatus() { ++ this.lastServerStatus = 0L; ++ } ++ ++ public void addTaskWithoutNotify(final Runnable run) { ++ this.globalTickQueue.add(run); ++ } ++ ++ public void addTask(final Runnable run) { ++ this.addTaskWithoutNotify(run); ++ TickRegions.getScheduler().setHasTasks(this.tickHandle); ++ } ++ ++ /** ++ * Returns the current tick of the region ticking. ++ * @throws IllegalStateException If there is no current region. ++ */ ++ public static long getCurrentTick() throws IllegalStateException { ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ if (TickThread.isShutdownThread()) { ++ return 0L; ++ } ++ throw new IllegalStateException("No currently ticking region"); ++ } ++ return region.getData().getCurrentTick(); ++ } ++ ++ public static boolean isGlobalTickThread() { ++ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); ++ } ++ ++ public static void ensureGlobalTickThread(final String reason) { ++ if (!isGlobalTickThread()) { ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { ++ return INSTANCE.tickHandle; ++ } ++ ++ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final RegionisedServer server; ++ ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ private final AtomicBoolean ticking = new AtomicBoolean(); ++ ++ public GlobalTickTickHandle(final RegionisedServer server) { ++ super(null, SchedulerThreadPool.DEADLINE_NOT_SET); ++ this.server = server; ++ } ++ ++ /** ++ * Only valid to call BEFORE scheduled!!!! ++ */ ++ final void setInitialStart(final long start) { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double scheduling global tick"); ++ } ++ this.updateScheduledStart(start); ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return !this.ticking.getAndSet(true); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.ticking.getAndSet(false); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ this.drainTasks(); ++ this.server.globalTick(tickCount); ++ } ++ ++ private void drainTasks() { ++ while (this.runOneTask()); ++ } ++ ++ private boolean runOneTask() { ++ final Runnable run = this.server.globalTickQueue.poll(); ++ if (run == null) { ++ return false; ++ } ++ ++ // TODO try catch? ++ run.run(); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ do { ++ if (!this.runOneTask()) { ++ return false; ++ } ++ } while (canContinue.getAsBoolean()); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return !this.server.globalTickQueue.isEmpty(); ++ } ++ } ++ ++ private long lastServerStatus; ++ private long tickCount; ++ ++ private void globalTick(final int tickCount) { ++ ++this.tickCount; ++ // commands ++ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); ++ ++ // needs ++ // player ping sample ++ // world global tick ++ // connection tick ++ ++ // tick player ping sample ++ this.tickPlayerSample(); ++ ++ // tick worlds ++ for (final ServerLevel world : this.worlds) { ++ this.globalTick(world, tickCount); ++ } ++ ++ // tick connections ++ this.tickConnections(); ++ ++ // player list ++ MinecraftServer.getServer().getPlayerList().tick(); ++ } ++ ++ private void tickPlayerSample() { ++ final MinecraftServer mcServer = MinecraftServer.getServer(); ++ final ServerStatus status = mcServer.getStatus(); ++ final PlayerList playerList = mcServer.getPlayerList(); ++ ++ final long i = System.nanoTime(); ++ ++ // player ping sample ++ // copied from MinecraftServer#tickServer ++ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing ++ // an uncomplete status ++ if (i - this.lastServerStatus >= 5000000000L) { ++ this.lastServerStatus = i; ++ List players = new ArrayList<>(playerList.players); ++ ServerStatus.Players newPlayers = new ServerStatus.Players(mcServer.getMaxPlayers(), players.size()); ++ ++ if (!mcServer.hidesOnlinePlayers()) { ++ GameProfile[] agameprofile = new GameProfile[Math.min(players.size(), org.spigotmc.SpigotConfig.playerSample)]; // Paper ++ int j = Mth.nextInt(new LegacyRandomSource(i), 0, players.size() - agameprofile.length); ++ ++ for (int k = 0; k < agameprofile.length; ++k) { ++ ServerPlayer entityplayer = (ServerPlayer) players.get(j + k); ++ ++ if (entityplayer.allowsListing()) { ++ agameprofile[k] = entityplayer.getGameProfile(); ++ } else { ++ agameprofile[k] = MinecraftServer.ANONYMOUS_PLAYER_PROFILE; ++ } ++ } ++ ++ Collections.shuffle(Arrays.asList(agameprofile)); ++ newPlayers.setSample(agameprofile); ++ } ++ // TODO make players field volatile ++ status.setPlayers(newPlayers); ++ } ++ } ++ ++ private boolean hasConnectionMovedToMain(final Connection conn) { ++ final PacketListener packetListener = conn.getPacketListener(); ++ ++ return (packetListener instanceof ServerGamePacketListenerImpl) || ++ (packetListener instanceof ServerLoginPacketListenerImpl loginListener && loginListener.state.ordinal() >= ServerLoginPacketListenerImpl.State.HANDING_OFF.ordinal()); ++ } ++ ++ private void tickConnections() { ++ final List connections = new ArrayList<>(this.connections); ++ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging ++ for (final Connection conn : connections) { ++ if (!conn.becomeActive()) { ++ continue; ++ } ++ ++ if (this.hasConnectionMovedToMain(conn)) { ++ if (!conn.isConnected()) { ++ this.removeConnection(conn); ++ } ++ continue; ++ } ++ ++ if (!conn.isConnected()) { ++ this.removeConnection(conn); ++ conn.handleDisconnection(); ++ continue; ++ } ++ ++ try { ++ conn.tick(); ++ } catch (final Exception exception) { ++ if (conn.isMemoryConnection()) { ++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); ++ } ++ ++ LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper ++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); ++ ++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { ++ conn.disconnect(ichatmutablecomponent); ++ })); ++ conn.setReadOnly(); ++ continue; ++ } ++ } ++ } ++ ++ // A global tick only updates things like weather / worldborder, basically anything in the world that is ++ // NOT tied to a specific region, but rather shared amongst all of them. ++ private void globalTick(final ServerLevel world, final int tickCount) { ++ // needs ++ // worldborder tick ++ // advancing the weather cycle ++ // sleep status thing ++ // updating sky brightness ++ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking ++ ++ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, ++ // this may not be the case - and thus, only the global tick thread can do anything. ++ world.taskQueueRegionData.drainGlobalChunkTasks(); ++ ++ // worldborder tick ++ this.tickWorldBorder(world); ++ ++ // weather cycle ++ this.advanceWeatherCycle(world); ++ ++ // sleep status TODO ++ ++ // sky brightness ++ this.updateSkyBrightness(world); ++ ++ // time ticking (TODO API synchronisation?) ++ this.tickTime(world, tickCount); ++ ++ world.updateTickData(); ++ } ++ ++ private void advanceWeatherCycle(final ServerLevel world) { ++ world.advanceWeatherCycle(); ++ } ++ ++ private void updateSkyBrightness(final ServerLevel world) { ++ world.updateSkyBrightness(); ++ } ++ ++ private void tickWorldBorder(final ServerLevel world) { ++ world.getWorldBorder().tick(); ++ } ++ ++ private void tickTime(final ServerLevel world, final int tickCount) { ++ if (world.tickTime) { ++ if (world.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ world.setDayTime(world.levelData.getDayTime() + (long)tickCount); ++ } ++ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); ++ } ++ } ++ ++ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c13237edb7323fa747d260375f626a5c9979b004 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java +@@ -0,0 +1,742 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.Iterator; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.ReentrantLock; ++ ++public final class RegionisedTaskQueue { ++ ++ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueChunkTask(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); ++ ret.queue(); ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); ++ ret.queue(); ++ return ret; ++ } ++ ++ public static final class WorldRegionTaskData { ++ private final ServerLevel world; ++ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); ++ private final SWMRLong2ObjectHashTable referenceCounters = new SWMRLong2ObjectHashTable<>(); ++ ++ public WorldRegionTaskData(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ private boolean executeGlobalChunkTask() { ++ final Runnable run = this.globalChunkTask.poll(); ++ if (run != null) { ++ run.run(); ++ return true; ++ } ++ return false; ++ } ++ ++ public void drainGlobalChunkTasks() { ++ while (this.executeGlobalChunkTask()); ++ } ++ ++ public void pushGlobalChunkTask(final Runnable run) { ++ this.globalChunkTask.add(run); ++ } ++ ++ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { ++ final ThreadedRegioniser regioniser = this.world.regioniser; ++ final ThreadedRegioniser.ThreadedRegion region ++ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); ++ if (region == null) { ++ return null; ++ } ++ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); ++ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); ++ } ++ ++ private void removeTicket(final long coord) { ++ this.world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void addTicket(final long coord) { ++ this.world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void decrementReference(final AtomicLong reference, final long coord) { ++ final long val = reference.decrementAndGet(); ++ if (val == 0L) { ++ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock; ++ ticketLock.lock(); ++ try { ++ if (this.referenceCounters.remove(coord, reference)) { ++ WorldRegionTaskData.this.removeTicket(coord); ++ } // else: race condition, something replaced our reference - not our issue anymore ++ } finally { ++ ticketLock.unlock(); ++ } ++ } else if (val < 0L) { ++ throw new IllegalStateException("Reference count < 0: " + val); ++ } ++ } ++ ++ private AtomicLong incrementReference(final long coord) { ++ final AtomicLong ret = this.referenceCounters.get(coord); ++ if (ret != null) { ++ // try to fast acquire counter ++ int failures = 0; ++ for (long curr = ret.get();;) { ++ if (curr == 0L) { ++ // failed to fast acquire as reference expired ++ break; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = ret.compareAndExchange(curr, curr + 1L))) { ++ return ret; ++ } ++ ++ ++failures; ++ } ++ } ++ ++ // slow acquire ++ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock; ++ ticketLock.lock(); ++ try { ++ final AtomicLong replace = new AtomicLong(1L); ++ final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(coord, replace); ++ if (valueInMap == null) { ++ // replaced, we should usually be here ++ this.addTicket(coord); ++ return replace; ++ } // else: need to attempt to acquire the reference ++ ++ int failures = 0; ++ for (long curr = valueInMap.get();;) { ++ if (curr == 0L) { ++ // don't need to add ticket here, since ticket is only removed during the lock ++ // we just need to replace the value in the map so that the thread removing fails and doesn't ++ // remove the ticket (see decrementReference) ++ this.referenceCounters.put(coord, replace); ++ return replace; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) { ++ // acquired ++ return valueInMap; ++ } ++ ++ ++failures; ++ } ++ } finally { ++ ticketLock.unlock(); ++ } ++ } ++ } ++ ++ public static final class RegionTaskQueueData { ++ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); ++ private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); ++ private final WorldRegionTaskData worldRegionTaskData; ++ ++ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { ++ this.worldRegionTaskData = worldRegionTaskData; ++ } ++ ++ void mergeInto(final RegionTaskQueueData into) { ++ this.tickTaskQueue.mergeInto(into.tickTaskQueue); ++ this.chunkQueue.mergeInto(into.chunkQueue); ++ } ++ ++ public boolean executeTickTask() { ++ return this.tickTaskQueue.executeTask(); ++ } ++ ++ public boolean executeChunkTask() { ++ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); ++ } ++ ++ void split(final ThreadedRegioniser regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ this.tickTaskQueue.split( ++ false, regioniser, into ++ ); ++ this.chunkQueue.split( ++ true, regioniser, into ++ ); ++ } ++ ++ public void drainTasks() { ++ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; ++ final PrioritisedQueue chunkTaskQueue = this.chunkQueue; ++ ++ int allowedTickTasks = tickTaskQueue.getScheduledTasks(); ++ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); ++ ++ boolean executeTickTasks = allowedTickTasks > 0; ++ boolean executeChunkTasks = allowedChunkTasks > 0; ++ boolean executeGlobalTasks = true; ++ ++ do { ++ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); ++ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); ++ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); ++ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); ++ } ++ ++ public boolean hasTasks() { ++ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); ++ } ++ } ++ ++ static final class PrioritisedQueue { ++ private final ArrayDeque[] queues = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { ++ for (int i = 0; i < PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { ++ this.queues[i] = new ArrayDeque<>(); ++ } ++ } ++ private boolean isDestroyed; ++ ++ public int getScheduledTasks() { ++ synchronized (this) { ++ int ret = 0; ++ ++ for (final ArrayDeque queue : this.queues) { ++ ret += queue.size(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ public boolean isEmpty() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = PrioritisedExecutor.Priority.IDLE.priority; ++ synchronized (this) { ++ for (int i = 0; i <= max; ++i) { ++ if (!queues[i].isEmpty()) { ++ return false; ++ } ++ } ++ return true; ++ } ++ } ++ ++ public void mergeInto(final PrioritisedQueue target) { ++ synchronized (this) { ++ this.isDestroyed = true; ++ synchronized (target) { ++ mergeInto(target, this.queues); ++ } ++ } ++ } ++ ++ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { ++ final ArrayDeque[] otherQueues = target.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ final ArrayDeque intoQ = otherQueues[i]; ++ ++ // it is possible for another thread to queue tasks into the target queue before we do ++ // since only the ticking region can poll, we don't have to worry about it when they are being queued - ++ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) ++ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the ++ // front of the queue, but we need to use descending iterator to ensure we do not reverse ++ // the order of elements from fromQ ++ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { ++ intoQ.addFirst(iterator.next()); ++ } ++ } ++ } ++ ++ // into is a map of section coordinate to region ++ public void split(final boolean isChunkData, ++ final ThreadedRegioniser regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ final Reference2ReferenceOpenHashMap, ArrayDeque[]> ++ split = new Reference2ReferenceOpenHashMap<>(); ++ final int shift = regioniser.sectionChunkShift; ++ synchronized (this) { ++ this.isDestroyed = true; ++ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting ++ ++ // first, build the targets ++ final ArrayDeque[] thisQueues = this.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ ++ for (final ChunkBasedPriorityTask task : fromQ) { ++ final int sectionX = task.chunkX >> shift; ++ final int sectionZ = task.chunkZ >> shift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final ThreadedRegioniser.ThreadedRegion ++ region = into.get(sectionKey); ++ if (region == null) { ++ throw new IllegalStateException(); ++ } ++ ++ split.computeIfAbsent(region, (keyInMap) -> { ++ final ArrayDeque[] ret = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ ++ for (int k = 0; k < ret.length; ++k) { ++ ret[k] = new ArrayDeque<>(); ++ } ++ ++ return ret; ++ })[i].add(task); ++ } ++ } ++ ++ // merge the targets into their queues ++ for (final Iterator, ArrayDeque[]>> ++ iterator = split.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, ArrayDeque[]> ++ entry = iterator.next(); ++ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); ++ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); ++ } ++ } ++ } ++ ++ /** ++ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true ++ * if the task was added ++ */ ++ private Boolean tryPush(final ChunkBasedPriorityTask task) { ++ final ArrayDeque[] queues = this.queues; ++ synchronized (this) { ++ final PrioritisedExecutor.Priority priority = task.getPriority(); ++ if (priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return null; ++ } ++ if (this.isDestroyed) { ++ return Boolean.FALSE; ++ } ++ queues[priority.priority].addLast(task); ++ return Boolean.TRUE; ++ } ++ } ++ ++ private boolean executeTask() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = PrioritisedExecutor.Priority.IDLE.priority; ++ ChunkBasedPriorityTask task = null; ++ AtomicLong referenceCounter = null; ++ synchronized (this) { ++ if (this.isDestroyed) { ++ throw new IllegalStateException("Attempting to poll from dead queue"); ++ } ++ ++ search_loop: ++ for (int i = 0; i <= max; ++i) { ++ final ArrayDeque queue = queues[i]; ++ while ((task = queue.pollFirst()) != null) { ++ if ((referenceCounter = task.trySetCompleting(i)) != null) { ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ if (task == null) { ++ return false; ++ } ++ ++ try { ++ task.executeInternal(); ++ } finally { ++ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); ++ } ++ ++ return true; ++ } ++ ++ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { ++ ++ private static final AtomicLong REFERENCE_COUNTER_NOT_SET = new AtomicLong(-1L); ++ ++ private final WorldRegionTaskData world; ++ private final int chunkX; ++ private final int chunkZ; ++ private final long sectionLowerLeftCoord; // chunk coordinate ++ private final boolean isChunkTask; ++ ++ private volatile AtomicLong referenceCounter; ++ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", AtomicLong.class); ++ private Runnable run; ++ private volatile PrioritisedExecutor.Priority priority; ++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", PrioritisedExecutor.Priority.class); ++ ++ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.isChunkTask = isChunkTask; ++ this.run = run; ++ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); ++ this.setPriorityPlain(priority); ++ ++ final int regionShift = world.world.regioniser.sectionChunkShift; ++ final int regionMask = (1 << regionShift) - 1; ++ ++ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); ++ } ++ ++ private PrioritisedExecutor.Priority getPriorityVolatile() { ++ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.getVolatile(this); ++ } ++ ++ private void setPriorityPlain(final PrioritisedExecutor.Priority priority) { ++ PRIORITY_HANDLE.set(this, priority); ++ } ++ ++ private void setPriorityVolatile(final PrioritisedExecutor.Priority priority) { ++ PRIORITY_HANDLE.setVolatile(this, priority); ++ } ++ ++ private PrioritisedExecutor.Priority compareAndExchangePriority(final PrioritisedExecutor.Priority expect, final PrioritisedExecutor.Priority update) { ++ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void setReferenceCounterPlain(final AtomicLong value) { ++ REFERENCE_COUNTER_HANDLE.set(this, value); ++ } ++ ++ private AtomicLong getReferenceCounterVolatile() { ++ return (AtomicLong)REFERENCE_COUNTER_HANDLE.get(this); ++ } ++ ++ private AtomicLong compareAndExchangeReferenceCounter(final AtomicLong expect, final AtomicLong update) { ++ return (AtomicLong)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void executeInternal() { ++ try { ++ this.run.run(); ++ } finally { ++ this.run = null; ++ } ++ } ++ ++ private void cancelInternal() { ++ this.run = null; ++ } ++ ++ private boolean tryComplete(final boolean cancel) { ++ int failures = 0; ++ for (AtomicLong curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ ++failures; ++ continue; ++ } ++ ++ // we have the reference count, we win no matter what. ++ this.setPriorityVolatile(PrioritisedExecutor.Priority.COMPLETING); ++ ++ try { ++ if (cancel) { ++ this.cancelInternal(); ++ } else { ++ this.executeInternal(); ++ } ++ } finally { ++ if (curr != REFERENCE_COUNTER_NOT_SET) { ++ this.world.decrementReference(curr, this.sectionLowerLeftCoord); ++ } ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public boolean queue() { ++ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { ++ return false; ++ } ++ ++ final AtomicLong referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); ++ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { ++ // we don't expect race conditions here, so it is OK if we have to needlessly reference count ++ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); ++ return false; ++ } ++ ++ boolean synchronise = false; ++ for (;;) { ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ continue; ++ } ++ // may have been cancelled before we got to the queue ++ if (this.getReferenceCounterVolatile() != null) { ++ throw new IllegalStateException("Expected null ref count when queue does not exist"); ++ } ++ // the task never could be polled from the queue, so we return false ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return true; ++ } ++ } ++ ++ private AtomicLong trySetCompleting(final int minPriority) { ++ // first, try to set priority to EXECUTING ++ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { ++ if (curr.isLowerPriority(minPriority)) { ++ return null; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, PrioritisedExecutor.Priority.COMPLETING))) { ++ break; ++ } // else: continue ++ } ++ ++ for (AtomicLong curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ // something acquired before us ++ return null; ++ } ++ ++ if (curr == REFERENCE_COUNTER_NOT_SET) { ++ throw new IllegalStateException(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ continue; ++ } ++ return curr; ++ } ++ } ++ ++ private void updatePriorityInQueue() { ++ boolean synchronise = false; ++ for (;;) { ++ final AtomicLong referenceCount = this.getReferenceCounterVolatile(); ++ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { ++ // cancelled or not queued ++ return; ++ } ++ ++ if (this.getPriorityVolatile() == PrioritisedExecutor.Priority.COMPLETING) { ++ // cancelled ++ return; ++ } ++ ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ continue; ++ } ++ // must have been removed ++ return; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ return; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return; ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.getPriorityVolatile(); ++ } ++ ++ @Override ++ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { ++ int failures = 0; ++ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean setPriority(final PrioritisedExecutor.Priority priority) { ++ int failures = 0; ++ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr == priority) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final PrioritisedExecutor.Priority priority) { ++ int failures = 0; ++ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { ++ if (curr == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean execute() { ++ return this.tryComplete(false); ++ } ++ ++ @Override ++ public boolean cancel() { ++ return this.tryComplete(true); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionisedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedWorldData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0ce7849e652f8093f061a87bbd48306102b66aa4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedWorldData.java +@@ -0,0 +1,652 @@ ++package io.papermc.paper.threadedregions; ++ ++import com.destroystokyo.paper.util.maplist.ReferenceList; ++import com.destroystokyo.paper.util.misc.PlayerAreaMap; ++import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.CrashReport; ++import net.minecraft.ReportedException; ++import net.minecraft.core.BlockPos; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.chat.Component; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.ai.village.VillageSiege; ++import net.minecraft.world.entity.item.ItemEntity; ++import net.minecraft.world.level.BlockEventData; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.TickingBlockEntity; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.gameevent.GameEvent; ++import net.minecraft.world.level.material.Fluid; ++import net.minecraft.world.level.redstone.CollectingNeighborUpdater; ++import net.minecraft.world.level.redstone.NeighborUpdater; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import net.minecraft.world.ticks.LevelTicks; ++import org.bukkit.craftbukkit.block.CraftBlockState; ++import org.bukkit.craftbukkit.util.UnsafeList; ++import org.bukkit.entity.SpawnCategory; ++import org.slf4j.Logger; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicReference; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++ ++public final class RegionisedWorldData { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final RegionisedData.RegioniserCallback REGION_CALLBACK = new RegionisedData.RegioniserCallback<>() { ++ @Override ++ public void merge(final RegionisedWorldData from, final RegionisedWorldData into, final long fromTickOffset) { ++ // connections ++ for (final Connection conn : from.connections) { ++ into.connections.add(conn); ++ } ++ // time ++ final long fromRedstoneTimeOffset = from.redstoneTime - into.redstoneTime; ++ // entities ++ for (final ServerPlayer player : from.localPlayers) { ++ into.localPlayers.add(player); ++ } ++ for (final Entity entity : from.allEntities) { ++ into.allEntities.add(entity); ++ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ } ++ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) { ++ into.entityTickList.add(iterator.next()); ++ } ++ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) { ++ into.navigatingMobs.add(iterator.next()); ++ } ++ // block ticking ++ into.blockEvents.addAll(from.blockEvents); ++ // ticklists use game time ++ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset); ++ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); ++ ++ // tile entity ticking ++ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { ++ into.pendingBlockEntityTickers.add(tileEntity); ++ //tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); // TODO ++ } ++ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { ++ into.blockEntityTickers.add(tileEntity); ++ //tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); // TODO ++ } ++ ++ // ticking chunks ++ for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) { ++ into.entityTickingChunks.add(iterator.next()); ++ } ++ for (final ChunkHolder holder : from.needsChangeBroadcasting) { ++ into.needsChangeBroadcasting.add(holder); ++ } ++ // redstone torches ++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { ++ if (into.redstoneUpdateInfos == null) { ++ into.redstoneUpdateInfos = new ArrayDeque<>(); ++ } ++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { ++ info.offsetTime(fromRedstoneTimeOffset); ++ into.redstoneUpdateInfos.add(info); ++ } ++ } ++ // light chunks being worked on ++ into.chunksBeingWorkedOn.putAll(from.chunksBeingWorkedOn); ++ // mob spawning ++ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick); ++ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick); ++ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick); ++ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) { ++ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay); ++ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); ++ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); ++ } ++ } ++ ++ @Override ++ public void split(final RegionisedWorldData from, final int chunkToRegionShift, ++ final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ // connections ++ for (final Connection conn : from.connections) { ++ final ServerPlayer player = conn.getPlayer(); ++ final ChunkPos pos = player.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .connections.add(conn); ++ } ++ // entities ++ for (final ServerPlayer player : from.localPlayers) { ++ final ChunkPos pos = player.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .localPlayers.add(player); ++ } ++ for (final Entity entity : from.allEntities) { ++ final ChunkPos pos = entity.chunkPosition(); ++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means ++ // the chunk holder must _exist_, and so the region section exists. ++ final RegionisedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); ++ into.allEntities.add(entity); ++ // Note: entityTickList is a subset of allEntities ++ if (from.entityTickList.contains(entity)) { ++ into.entityTickList.add(entity); ++ } ++ // Note: navigatingMobs is a subset of allEntities ++ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) { ++ into.navigatingMobs.add(mob); ++ } ++ } ++ // block ticking ++ for (final BlockEventData blockEventData : from.blockEvents) { ++ final BlockPos pos = blockEventData.pos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionisedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events ++ // is just some list. So if it unloads, I guess it's just lost. ++ if (into != null) { ++ into.blockEvents.add(blockEventData); ++ } ++ } ++ ++ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); ++ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); ++ ++ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ReferenceMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final RegionisedWorldData worldData = entry.getValue(); ++ ++ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks); ++ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks); ++ } ++ ++ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData); ++ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); ++ ++ // tile entity ticking ++ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { ++ final BlockPos pos = tileEntity.getPos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionisedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ if (into != null) { ++ into.pendingBlockEntityTickers.add(tileEntity); ++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets ++ // marked as removed. So if there is no section, it's probably removed! ++ } ++ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { ++ final BlockPos pos = tileEntity.getPos(); ++ final int chunkX = pos.getX() >> 4; ++ final int chunkZ = pos.getZ() >> 4; ++ ++ final RegionisedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); ++ if (into != null) { ++ into.blockEntityTickers.add(tileEntity); ++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets ++ // marked as removed. So if there is no section, it's probably removed! ++ } ++ // time ++ for (final RegionisedWorldData regionisedWorldData : dataSet) { ++ regionisedWorldData.redstoneTime = from.redstoneTime; ++ } ++ // ticking chunks ++ for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) { ++ final LevelChunk levelChunk = iterator.next(); ++ final ChunkPos pos = levelChunk.getPos(); ++ ++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .entityTickingChunks.add(levelChunk); ++ } ++ ++ for (final ChunkHolder holder : from.needsChangeBroadcasting) { ++ final ChunkPos pos = holder.pos; ++ ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) ++ .needsChangeBroadcasting.add(holder); ++ } ++ // redstone torches ++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { ++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { ++ final BlockPos pos = info.pos; ++ ++ final RegionisedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift)); ++ if (worldData != null) { ++ if (worldData.redstoneUpdateInfos == null) { ++ worldData.redstoneUpdateInfos = new ArrayDeque<>(); ++ } ++ worldData.redstoneUpdateInfos.add(info); ++ } // else: chunk unloaded ++ } ++ } ++ // light chunks being worked on ++ for (final Iterator iterator = from.chunksBeingWorkedOn.long2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ final Long2IntOpenHashMap.Entry entry = iterator.next(); ++ final long pos = entry.getLongKey(); ++ final int chunkX = CoordinateUtils.getChunkX(pos); ++ final int chunkZ = CoordinateUtils.getChunkZ(pos); ++ final int value = entry.getIntValue(); ++ ++ // should never be null, as it is a reference counter for ticket ++ regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)).chunksBeingWorkedOn.put(pos, value); ++ } ++ // mob spawning ++ for (final RegionisedWorldData regionisedWorldData : dataSet) { ++ regionisedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; ++ regionisedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; ++ regionisedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; ++ regionisedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; ++ regionisedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; ++ regionisedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; ++ regionisedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid ++ } ++ } ++ }; ++ ++ public final ServerLevel world; ++ ++ private RegionisedServer.WorldLevelData tickData; ++ ++ // connections ++ public final List connections = new ArrayList<>(); ++ ++ // misc. fields ++ private boolean isHandlingTick; ++ ++ public void setHandlingTick(final boolean to) { ++ this.isHandlingTick = to; ++ } ++ ++ public boolean isHandlingTick() { ++ return this.isHandlingTick; ++ } ++ ++ // entities ++ private final List localPlayers = new ArrayList<>(); ++ private final ReferenceList allEntities = new ReferenceList<>(); ++ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>(); ++ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); ++ ++ // block ticking ++ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); ++ private final LevelTicks blockLevelTicks; ++ private final LevelTicks fluidLevelTicks; ++ ++ // tile entity ticking ++ private final List pendingBlockEntityTickers = new ArrayList<>(); ++ private final List blockEntityTickers = new ArrayList<>(); ++ private boolean tickingBlockEntities; ++ ++ // time ++ private long redstoneTime = 1L; ++ ++ public long getRedstoneGameTime() { ++ return this.redstoneTime; ++ } ++ ++ public void setRedstoneGameTime(final long to) { ++ this.redstoneTime = to; ++ } ++ ++ // ticking chunks ++ private final IteratorSafeOrderedReferenceSet entityTickingChunks = new IteratorSafeOrderedReferenceSet<>(); ++ private final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); ++ ++ // Paper/CB api hook misc ++ // don't bother to merge/split these, no point ++ // From ServerLevel ++ public boolean hasPhysicsEvent = true; // Paper ++ public boolean hasEntityMoveEvent = false; // Paper ++ public long lastMidTickExecuteFailure; ++ public long lastMidTickExecute; ++ // From Level ++ public boolean populating; ++ public final NeighborUpdater neighborUpdater; ++ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 ++ public boolean captureBlockStates = false; ++ public boolean captureTreeGeneration = false; ++ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper ++ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper ++ public List captureDrops; ++ // Paper start ++ public int wakeupInactiveRemainingAnimals; ++ public int wakeupInactiveRemainingFlying; ++ public int wakeupInactiveRemainingMonsters; ++ public int wakeupInactiveRemainingVillagers; ++ // Paper end ++ public final TempCollisionList tempCollisionList = new TempCollisionList<>(); ++ public final TempCollisionList tempEntitiesList = new TempCollisionList<>(); ++ public int currentPrimedTnt = 0; // Spigot ++ ++ // not transient ++ public java.util.ArrayDeque redstoneUpdateInfos; ++ public final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); ++ ++ public static final class TempCollisionList { ++ final UnsafeList list = new UnsafeList<>(64); ++ boolean inUse; ++ ++ public UnsafeList get() { ++ if (this.inUse) { ++ return new UnsafeList<>(16); ++ } ++ this.inUse = true; ++ return this.list; ++ } ++ ++ public void ret(List list) { ++ if (list != this.list) { ++ return; ++ } ++ ++ ((UnsafeList)list).setSize(0); ++ this.inUse = false; ++ } ++ ++ public void reset() { ++ this.list.completeReset(); ++ } ++ } ++ public void resetCollisionLists() { ++ this.tempCollisionList.reset(); ++ this.tempEntitiesList.reset(); ++ } ++ ++ // Mob spawning ++ private final PooledLinkedHashSets pooledHashSets = new PooledLinkedHashSets<>(); ++ public final PlayerAreaMap mobSpawnMap = new PlayerAreaMap(this.pooledHashSets); ++ public int catSpawnerNextTick = 0; ++ public int patrolSpawnerNextTick = 0; ++ public int phantomSpawnerNextTick = 0; ++ public int wanderingTraderTickDelay = Integer.MIN_VALUE; ++ public int wanderingTraderSpawnDelay; ++ public int wanderingTraderSpawnChance; ++ public VillageSiegeState villageSiegeState = new VillageSiegeState(); ++ ++ public static final class VillageSiegeState { ++ public boolean hasSetupSiege; ++ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; ++ public int zombiesToSpawn; ++ public int nextSpawnTime; ++ public int spawnX; ++ public int spawnY; ++ public int spawnZ; ++ } ++ ++ public RegionisedWorldData(final ServerLevel world) { ++ this.world = world; ++ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, true); ++ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, false); ++ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax); ++ ++ // tasks may be drained before the region ticks, so we must set up the tick data early just in case ++ this.updateTickData(); ++ } ++ ++ public RegionisedServer.WorldLevelData getTickData() { ++ return this.tickData; ++ } ++ ++ public void updateTickData() { ++ this.tickData = this.world.tickData; ++ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ } ++ ++ // connections ++ public void tickConnections() { ++ final List connections = new ArrayList<>(this.connections); ++ Collections.shuffle(connections); ++ for (final Connection conn : connections) { ++ if (!conn.isConnected()) { ++ conn.handleDisconnection(); ++ this.connections.remove(conn); ++ // note: ALL connections HERE have a player ++ final ServerPlayer player = conn.getPlayer(); ++ // now that the connection is removed, we can allow this region to die ++ player.getLevel().chunkSource.removeTicketAtLevel( ++ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos, ++ ChunkHolderManager.MAX_TICKET_LEVEL, ++ player.connection.disconnectTicketId ++ ); ++ continue; ++ } ++ if (!this.connections.contains(conn)) { ++ // removed by connection tick? ++ continue; ++ } ++ ++ try { ++ conn.tick(); ++ } catch (final Exception exception) { ++ if (conn.isMemoryConnection()) { ++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); ++ } ++ ++ LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper ++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); ++ ++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { ++ conn.disconnect(ichatmutablecomponent); ++ })); ++ conn.setReadOnly(); ++ continue; ++ } ++ } ++ } ++ ++ // entities hooks ++ public Iterable getLocalEntities() { ++ return this.allEntities; ++ } ++ ++ public Entity[] getLocalEntitiesCopy() { ++ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class); ++ } ++ ++ public List getLocalPlayers() { ++ return this.localPlayers; ++ } ++ ++ public void addEntityTickingEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ this.entityTickList.add(entity); ++ } ++ ++ public boolean hasEntityTickingEntity(final Entity entity) { ++ return this.entityTickList.contains(entity); ++ } ++ ++ public void removeEntityTickingEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ this.entityTickList.remove(entity); ++ } ++ ++ public void forEachTickingEntity(final Consumer action) { ++ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator(); ++ try { ++ while (iterator.hasNext()) { ++ action.accept(iterator.next()); ++ } ++ } finally { ++ iterator.finishedIterating(); ++ } ++ } ++ ++ public void addEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ if (this.allEntities.add(entity)) { ++ if (entity instanceof ServerPlayer player) { ++ this.localPlayers.add(player); ++ } ++ } ++ } ++ ++ public boolean hasEntity(final Entity entity) { ++ return this.allEntities.contains(entity); ++ } ++ ++ public void removeEntity(final Entity entity) { ++ if (!TickThread.isTickThreadFor(entity)) { ++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); ++ } ++ if (this.allEntities.remove(entity)) { ++ if (entity instanceof ServerPlayer player) { ++ this.localPlayers.remove(player); ++ } ++ } ++ } ++ ++ public void addNavigatingMob(final Mob mob) { ++ if (!TickThread.isTickThreadFor(mob)) { ++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); ++ } ++ this.navigatingMobs.add(mob); ++ } ++ ++ public void removeNavigatingMob(final Mob mob) { ++ if (!TickThread.isTickThreadFor(mob)) { ++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); ++ } ++ this.navigatingMobs.remove(mob); ++ } ++ ++ public Iterator getNavigatingMobs() { ++ return this.navigatingMobs.unsafeIterator(); ++ } ++ ++ // block ticking hooks ++ // Since block event data does not require chunk holders to be created for the chunk they reside in, ++ // it's not actually guaranteed that when merging / splitting data that we actually own the data... ++ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to ++ // make the code easier by simply discarding it in such an event ++ public void pushBlockEvent(final BlockEventData blockEventData) { ++ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async"); ++ this.blockEvents.add(blockEventData); ++ } ++ ++ public void pushBlockEvents(final Collection blockEvents) { ++ for (final BlockEventData blockEventData : blockEvents) { ++ this.pushBlockEvent(blockEventData); ++ } ++ } ++ ++ public void removeIfBlockEvents(final Predicate predicate) { ++ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { ++ final BlockEventData blockEventData = iterator.next(); ++ if (predicate.test(blockEventData)) { ++ iterator.remove(); ++ } ++ } ++ } ++ ++ public BlockEventData removeFirstBlockEvent() { ++ BlockEventData ret; ++ while (!this.blockEvents.isEmpty()) { ++ ret = this.blockEvents.removeFirst(); ++ if (TickThread.isTickThreadFor(this.world, ret.pos())) { ++ return ret; ++ } // else: chunk must have been unloaded ++ } ++ ++ return null; ++ } ++ ++ public LevelTicks getBlockLevelTicks() { ++ return this.blockLevelTicks; ++ } ++ ++ public LevelTicks getFluidLevelTicks() { ++ return this.fluidLevelTicks; ++ } ++ ++ // tile entity ticking ++ public void addBlockEntityTicker(final TickingBlockEntity ticker) { ++ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); ++ ++ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ } ++ ++ public void seTtickingBlockEntities(final boolean to) { ++ this.tickingBlockEntities = true; ++ } ++ ++ public List getBlockEntityTickers() { ++ return this.blockEntityTickers; ++ } ++ ++ public void pushPendingTickingBlockEntities() { ++ if (!this.pendingBlockEntityTickers.isEmpty()) { ++ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); ++ this.pendingBlockEntityTickers.clear(); ++ } ++ } ++ ++ // ticking chunks ++ public void addEntityTickingChunks(final LevelChunk levelChunk) { ++ this.entityTickingChunks.add(levelChunk); ++ } ++ ++ public void removeEntityTickingChunk(final LevelChunk levelChunk) { ++ this.entityTickingChunks.remove(levelChunk); ++ } ++ ++ public IteratorSafeOrderedReferenceSet getEntityTickingChunks() { ++ return this.entityTickingChunks; ++ } ++ ++ public void addChunkHolderNeedsBroadcasting(final ChunkHolder holder) { ++ this.needsChangeBroadcasting.add(holder); ++ } ++ ++ public void removeChunkHolderNeedsBroadcasting(final ChunkHolder holder) { ++ this.needsChangeBroadcasting.remove(holder); ++ } ++ ++ public ReferenceOpenHashSet getNeedsChangeBroadcasting() { ++ return this.needsChangeBroadcasting; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java +new file mode 100644 +index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java +@@ -0,0 +1,91 @@ ++package io.papermc.paper.threadedregions; ++ ++/** ++ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. ++ */ ++public final class Schedule { ++ ++ private long lastPeriod; ++ ++ /** ++ * Initialises a schedule with the provided period. ++ * @param firstPeriod The last time an event of interest occurred. ++ * @see #setLastPeriod(long) ++ */ ++ public Schedule(final long firstPeriod) { ++ this.lastPeriod = firstPeriod; ++ } ++ ++ /** ++ * Updates the last period to the specified value. This call sets the last "time" the event ++ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is ++ * the provided time plus the period length provided to {@code getDeadline}. ++ * @param value The value to set the last period to. ++ */ ++ public void setLastPeriod(final long value) { ++ this.lastPeriod = value; ++ } ++ ++ /** ++ * Returns the last time the event of interest should have taken place. ++ */ ++ public long getLastPeriod() { ++ return this.lastPeriod; ++ } ++ ++ /** ++ * Returns the number of times the event of interest should have taken place between the last ++ * period and the provided time given the period between each event. ++ * @param periodLength The length of the period between events in ns. ++ * @param time The provided time. ++ */ ++ public int getPeriodsAhead(final long periodLength, final long time) { ++ final long difference = time - this.lastPeriod; ++ final int ret = (int)(Math.abs(difference) / periodLength); ++ return difference >= 0 ? ret : -ret; ++ } ++ ++ /** ++ * Returns the next starting deadline for the event of interest to take place, ++ * given the provided period length. ++ * @param periodLength The provided period length. ++ */ ++ public long getDeadline(final long periodLength) { ++ return this.lastPeriod + periodLength; ++ } ++ ++ /** ++ * Adjusts the last period so that the next starting deadline returned is the next period specified, ++ * given the provided period length. ++ * @param nextPeriod The specified next starting deadline. ++ * @param periodLength The specified period length. ++ */ ++ public void setNextPeriod(final long nextPeriod, final long periodLength) { ++ this.lastPeriod = nextPeriod - periodLength; ++ } ++ ++ /** ++ * Increases the last period by the specified number of periods and period length. ++ * The specified number of periods may be < 0, in which case the last period ++ * will decrease. ++ * @param periods The specified number of periods. ++ * @param periodLength The specified period length. ++ */ ++ public void advanceBy(final int periods, final long periodLength) { ++ this.lastPeriod += (long)periods * periodLength; ++ } ++ ++ /** ++ * Sets the last period so that it is the specified number of periods ahead ++ * given the specified time and period length. ++ * @param periodsToBeAhead Specified number of periods to be ahead by. ++ * @param periodLength The specified period length. ++ * @param time The specified time. ++ */ ++ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { ++ final int periodsAhead = this.getPeriodsAhead(periodLength, time); ++ final int periodsToAdd = periodsToBeAhead - periodsAhead; ++ ++ this.lastPeriod -= (long)periodsToAdd * periodLength; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64d67c2c6c67fa64582b4f8516bd2350f4f034e5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java +@@ -0,0 +1,60 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.phys.Vec3; ++import org.bukkit.Location; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.event.player.PlayerTeleportEvent; ++import java.util.function.Consumer; ++ ++public final class TeleportUtils { ++ ++ public static void teleport(final Entity from, final Entity to, final Float yaw, final Float pitch, ++ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { ++ // retrieve coordinates ++ final Completable positionCompletable = new Completable<>(); ++ ++ positionCompletable.addWaiter( ++ (final Location loc, final Throwable thr) -> { ++ if (loc == null) { ++ onComplete.accept(null); ++ return; ++ } ++ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( ++ (final Entity realFrom) -> { ++ final Vec3 pos = new Vec3( ++ loc.getX(), loc.getY(), loc.getZ() ++ ); ++ realFrom.teleportAsync( ++ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, ++ cause, teleportFlags, onComplete ++ ); ++ }, ++ (final Entity retired) -> { ++ onComplete.accept(null); ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ onComplete.accept(null); ++ } ++ } ++ ); ++ ++ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( ++ (final Entity target) -> { ++ positionCompletable.complete(target.getBukkitEntity().getLocation()); ++ }, ++ (final Entity retired) -> { ++ onComplete.accept(null); ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ onComplete.accept(null); ++ } ++ } ++ ++ private TeleportUtils() {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegioniser.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegioniser.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f05546aa9124d4c0e34005f528483bf516e93c20 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegioniser.java +@@ -0,0 +1,1187 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonParser; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++ ++import java.io.FileReader; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.StampedLock; ++import java.util.function.Consumer; ++ ++public final class ThreadedRegioniser, S extends ThreadedRegioniser.ThreadedRegionSectionData> { ++ ++ public final int regionSectionChunkSize; ++ public final int sectionChunkShift; ++ public final int minSectionRecalcCount; ++ public final int emptySectionCreateRadius; ++ public final int regionSectionMergeRadius; ++ public final double maxDeadRegionPercent; ++ public final ServerLevel world; ++ ++ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); ++ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); ++ private final RegionCallbacks callbacks; ++ private final StampedLock regionLock = new StampedLock(); ++ private Thread writeLockOwner; ++ ++ /* ++ static final record Operation(String type, int chunkX, int chunkZ) {} ++ private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); ++ */ ++ ++ public ThreadedRegioniser(final int minSectionRecalcCount, final double maxDeadRegionPercent, ++ final int emptySectionCreateRadius, final int regionSectionMergeRadius, ++ final int regionSectionChunkShift, final ServerLevel world, ++ final RegionCallbacks callbacks) { ++ if (emptySectionCreateRadius <= 0) { ++ throw new IllegalStateException("Region section create radius must be > 0"); ++ } ++ if (regionSectionMergeRadius <= 0) { ++ throw new IllegalStateException("Region section merge radius must be > 0"); ++ } ++ this.regionSectionChunkSize = 1 << regionSectionChunkShift; ++ this.sectionChunkShift = regionSectionChunkShift; ++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); ++ this.maxDeadRegionPercent = maxDeadRegionPercent; ++ this.emptySectionCreateRadius = emptySectionCreateRadius; ++ this.regionSectionMergeRadius = regionSectionMergeRadius; ++ this.world = world; ++ this.callbacks = callbacks; ++ //this.loadTestData(); ++ } ++ ++ /* ++ private static String substr(String val, String prefix, int from) { ++ int idx = val.indexOf(prefix, from) + prefix.length(); ++ int idx2 = val.indexOf(',', idx); ++ if (idx2 == -1) { ++ idx2 = val.indexOf(']', idx); ++ } ++ return val.substring(idx, idx2); ++ } ++ ++ private void loadTestData() { ++ if (true) { ++ return; ++ } ++ try { ++ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); ++ ++ List ops = new ArrayList<>(); ++ ++ for (JsonElement elem : arr) { ++ JsonObject obj = elem.getAsJsonObject(); ++ String val = obj.get("value").getAsString(); ++ ++ String type = substr(val, "type=", 0); ++ String x = substr(val, "chunkX=", 0); ++ String z = substr(val, "chunkZ=", 0); ++ ++ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); ++ } ++ ++ for (Operation op : ops) { ++ switch (op.type) { ++ case "add": { ++ this.addChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "remove": { ++ this.removeChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "mark_ticking": { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); ++ break; ++ } ++ case "rel_region": { ++ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); ++ } ++ break; ++ } ++ } ++ } ++ ++ } catch (final Exception ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ */ ++ ++ private void acquireWriteLock() { ++ final Thread currentThread = Thread.currentThread(); ++ if (this.writeLockOwner == currentThread) { ++ throw new IllegalStateException("Cannot recursively operate in the regioniser"); ++ } ++ this.regionLock.writeLock(); ++ this.writeLockOwner = currentThread; ++ } ++ ++ private void releaseWriteLock() { ++ this.writeLockOwner = null; ++ this.regionLock.tryUnlockWrite(); ++ } ++ ++ private void onRegionCreate(final ThreadedRegion region) { ++ final ThreadedRegion conflict; ++ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { ++ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); ++ } ++ } ++ ++ private void onRegionDestroy(final ThreadedRegion region) { ++ final ThreadedRegion removed = this.regionsById.remove(region.id); ++ if (removed != region) { ++ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); ++ } ++ } ++ ++ public int getSectionCoordinate(final int chunkCoordinate) { ++ return chunkCoordinate >> this.sectionChunkShift; ++ } ++ ++ public long getSectionKey(final BlockPos pos) { ++ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final ChunkPos pos) { ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final Entity entity) { ++ final ChunkPos pos = entity.chunkPosition(); ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public void computeForAllRegions(final Consumer> consumer) { ++ this.regionLock.readLock(); ++ try { ++ this.regionsById.forEachValue(consumer); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { ++ this.regionsById.forEachValue(consumer); ++ } ++ ++ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegion(); ++ } ++ ++ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // try an optimistic read ++ { ++ final long readAttempt = this.regionLock.tryOptimisticRead(); ++ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); ++ final ThreadedRegion optimisticRet = ++ optimisticSection == null ? null : optimisticSection.getRegionPlain(); ++ if (this.regionLock.validate(readAttempt)) { ++ return optimisticRet; ++ } ++ } ++ ++ // failed, fall back to acquiring the lock ++ this.regionLock.readLock(); ++ try { ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegionPlain(); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ /** ++ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless ++ * addChunk has not been called for it or removeChunk has been previously called. ++ * ++ *

    ++ * Note that it is illegal to additionally call addChunk or removeChunk for the same ++ * region section in parallel. ++ *

    ++ */ ++ public void addChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a non-zero chunk count. ++ { ++ final ThreadedRegionSection existing = this.sections.get(sectionKey); ++ if (existing != null && !existing.isEmpty()) { ++ existing.addChunk(chunkX, chunkZ); ++ return; ++ } // else: just acquire the write lock ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ List> newSections = new ArrayList<>(); ++ ++ if (section == null) { ++ // no section at all ++ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); ++ this.sections.put(sectionKey, section); ++ newSections.add(section); ++ } else { ++ section.addChunk(chunkX, chunkZ); ++ } ++ // due to the fast check from above, we know the section is empty whether we need to create it ++ ++ // enforce the adjacency invariant by creating / updating neighbour sections ++ final int createRadius = this.emptySectionCreateRadius; ++ final int searchRadius = createRadius + this.regionSectionMergeRadius; ++ ReferenceOpenHashSet> nearbyRegions = null; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final boolean inCreateRange = squareDistance <= createRadius; ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ if (neighbourSection != null) { ++ if (nearbyRegions == null) { ++ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); ++ } ++ nearbyRegions.add(neighbourSection.getRegionPlain()); ++ } ++ ++ if (!inCreateRange) { ++ continue; ++ } ++ ++ // we need to ensure the section exists ++ if (neighbourSection != null) { ++ // nothing else to do ++ neighbourSection.incrementNonEmptyNeighbours(); ++ continue; ++ } ++ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); ++ if (null != this.sections.put(neighbourKey, neighbourSection)) { ++ throw new IllegalStateException("Failed to insert new section"); ++ } ++ newSections.add(neighbourSection); ++ } ++ } ++ ++ final ThreadedRegion regionOfInterest; ++ final boolean regionOfInterestAlive; ++ if (nearbyRegions == null) { ++ // we can simply create a new region, don't have neighbours to worry about merging into ++ regionOfInterest = new ThreadedRegion<>(this); ++ regionOfInterestAlive = true; ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ regionOfInterest.onCreate(); ++ } else { ++ // need to merge the regions ++ ++ ThreadedRegion firstUnlockedRegion = null; ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region.isTicking()) { ++ continue; ++ } ++ firstUnlockedRegion = region; ++ break; ++ } ++ ++ if (firstUnlockedRegion != null) { ++ regionOfInterest = firstUnlockedRegion; ++ } else { ++ regionOfInterest = new ThreadedRegion<>(this); ++ } ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ if (firstUnlockedRegion == null) { ++ regionOfInterest.onCreate(); ++ } ++ ++ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { ++ // nothing to do further, no need to merge anything ++ return; ++ } ++ ++ // we need to now tell all the other regions to merge into the region we just created, ++ // and to merge all the ones we can immediately ++ ++ boolean delayedTrueMerge = false; ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region == regionOfInterest) { ++ continue; ++ } ++ // need the relaxed check, as the region may already be ++ // a merge target ++ if (!region.tryKill()) { ++ regionOfInterest.mergeIntoLater(region); ++ delayedTrueMerge = true; ++ } else { ++ region.mergeInto(regionOfInterest); ++ } ++ } ++ ++ if (delayedTrueMerge && firstUnlockedRegion != null) { ++ // we need to retire this region, as it can no longer tick ++ if (regionOfInterest.state == ThreadedRegion.STATE_STEADY_STATE) { ++ regionOfInterest.state = ThreadedRegion.STATE_NOT_READY; ++ this.callbacks.onRegionInactive(regionOfInterest); ++ } ++ } ++ ++ // need to set alive if we created it and we didn't delay a merge ++ regionOfInterestAlive = firstUnlockedRegion == null && !delayedTrueMerge && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); ++ } ++ ++ if (regionOfInterestAlive) { ++ regionOfInterest.state = ThreadedRegion.STATE_STEADY_STATE; ++ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Should not happen on region " + this); ++ } ++ this.callbacks.onRegionActive(regionOfInterest); ++ } ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ public void removeChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a chunk count > 1 ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ if (section == null) { ++ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); ++ } ++ if (!section.hasOnlyOneChunk()) { ++ // chunk will not go empty, so we don't need to acquire the lock ++ section.removeChunk(chunkX, chunkZ); ++ return; ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ section.removeChunk(chunkX, chunkZ); ++ ++ final int searchRadius = this.emptySectionCreateRadius; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ // should be non-null here always ++ neighbourSection.decrementNonEmptyNeighbours(); ++ } ++ } ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ // must hold regionLock ++ private void onRegionRelease(final ThreadedRegion region) { ++ if (!region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); ++ } ++ ++ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); ++ ++ // is this region supposed to merge into any other region? ++ if (hasExpectingMerges) { ++ // merge the regions into this one ++ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); ++ for (final ThreadedRegion mergeFrom : expectingMergeFrom) { ++ if (!mergeFrom.tryKill()) { ++ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); ++ } ++ mergeFrom.mergeInto(region); ++ } ++ ++ if (!region.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); ++ } ++ ++ if (!region.mergeIntoLater.isEmpty()) { ++ // There is another nearby ticking region that we need to merge into ++ region.state = ThreadedRegion.STATE_NOT_READY; ++ this.callbacks.onRegionInactive(region); ++ // return to avoid removing dead sections or splitting, these actions will be performed ++ // by the region we merge into ++ return; ++ } ++ } ++ ++ // now check whether we need to recalculate regions ++ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() ++ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); ++ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); ++ if (removeDeadSections) { ++ // kill dead sections ++ for (final ThreadedRegionSection deadSection : region.deadSections) { ++ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); ++ ++ if (!deadSection.isEmpty()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); ++ } ++ if (deadSection.hasNonEmptyNeighbours()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); ++ } ++ if (!region.sectionByKey.remove(key, deadSection)) { ++ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); ++ } ++ if (this.sections.remove(key) != deadSection) { ++ throw new IllegalStateException("Cannot remove dead section '" + ++ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); ++ } ++ } ++ region.deadSections.clear(); ++ } ++ ++ // if we removed dead sections, we should check if the region can be split into smaller ones ++ // otherwise, the region remains alive ++ if (!removedDeadSections) { ++ region.state = ThreadedRegion.STATE_STEADY_STATE; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ // first, we need to build copy of coordinate->section map of all sections in recalculate ++ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); ++ ++ if (recalculateSections.isEmpty()) { ++ // looks like the region's sections were all dead, and now there is no region at all ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ return; ++ } ++ ++ // merge radius is max, since recalculateSections includes the dead or empty sections ++ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); ++ ++ final List>> newRegions = new ArrayList<>(); ++ while (!recalculateSections.isEmpty()) { ++ // select any section, then BFS around it to find all of its neighbours to form a region ++ // once no more neighbours are found, the region is complete ++ final List> currRegion = new ArrayList<>(); ++ final Iterator> firstIterator = recalculateSections.values().iterator(); ++ ++ currRegion.add(firstIterator.next()); ++ firstIterator.remove(); ++ search_loop: ++ for (int idx = 0; idx < currRegion.size(); ++idx) { ++ final ThreadedRegionSection curr = currRegion.get(idx); ++ final int centerX = curr.sectionX; ++ final int centerZ = curr.sectionZ; ++ ++ // find neighbours in radius ++ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { ++ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); ++ if (section == null) { ++ continue; ++ } ++ ++ currRegion.add(section); ++ ++ if (recalculateSections.isEmpty()) { ++ // no point in searching further ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ newRegions.add(currRegion); ++ } ++ ++ // now we have split the regions into separate parts, we can split recalculate ++ ++ if (newRegions.size() == 1) { ++ // no need to split anything, we're done here ++ region.state = ThreadedRegion.STATE_STEADY_STATE; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ // need to split the region, so we need to kill the old one first ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ ++ // create new regions ++ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); ++ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(); ++ ++ for (final List> sections : newRegions) { ++ final ThreadedRegion newRegion = new ThreadedRegion<>(this); ++ newRegionsSet.add(newRegion); ++ ++ for (final ThreadedRegionSection section : sections) { ++ section.setRegionRelease(null); ++ newRegion.addSection(section); ++ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); ++ if (curr != null) { ++ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); ++ } ++ } ++ } ++ ++ region.split(newRegionsMap, newRegionsSet); ++ ++ // only after invoking data callbacks ++ ++ for (final ThreadedRegion newRegion : newRegionsSet) { ++ newRegion.state = ThreadedRegion.STATE_STEADY_STATE; ++ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + newRegion); ++ } ++ newRegion.onCreate(); ++ this.callbacks.onRegionActive(newRegion); ++ } ++ } ++ ++ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { ++ ++ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); ++ ++ private static final int STATE_NOT_READY = 0; ++ private static final int STATE_STEADY_STATE = 1; ++ private static final int STATE_TICKING = 2; ++ private static final int STATE_DEAD = 3; ++ ++ public final long id; ++ ++ private int state; ++ ++ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); ++ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); ++ ++ public final ThreadedRegioniser regioniser; ++ ++ private final R data; ++ ++ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); ++ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); ++ ++ public ThreadedRegion(final ThreadedRegioniser regioniser) { ++ this.regioniser = regioniser; ++ this.id = REGION_ID_GENERATOR.getAndIncrement(); ++ this.state = STATE_NOT_READY; ++ this.data = regioniser.callbacks.createNewData(this); ++ } ++ ++ public LongArrayList getOwnedSections() { ++ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); ++ if (lock) { ++ this.regioniser.regionLock.readLock(); ++ } ++ try { ++ final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); ++ ret.addAll(this.sectionByKey.keySet()); ++ ++ return ret; ++ } finally { ++ if (lock) { ++ this.regioniser.regionLock.tryUnlockRead(); ++ } ++ } ++ } ++ ++ public ChunkPos getCenterChunk() { ++ final LongArrayList sections = this.getOwnedSections(); ++ ++ sections.sort(null); ++ ++ // note: regions always have at least one section ++ final long middle = sections.getLong(sections.size() >> 1); ++ ++ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); ++ } ++ ++ private void onCreate() { ++ this.regioniser.onRegionCreate(this); ++ this.regioniser.callbacks.onRegionCreate(this); ++ } ++ ++ private void onRemove(final boolean wasActive) { ++ if (wasActive) { ++ this.regioniser.callbacks.onRegionInactive(this); ++ } ++ this.regioniser.callbacks.onRegionDestroy(this); ++ this.regioniser.onRegionDestroy(this); ++ } ++ ++ private final boolean hasNoAliveSections() { ++ return this.deadSections.size() == this.sectionByKey.size(); ++ } ++ ++ private final double getDeadSectionPercent() { ++ return (double)this.deadSections.size() / (double)this.sectionByKey.size(); ++ } ++ ++ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { ++ if (this.data != null) { ++ this.data.split(this.regioniser, into, regions); ++ } ++ } ++ ++ private void mergeInto(final ThreadedRegion mergeTarget) { ++ if (this == mergeTarget) { ++ throw new IllegalStateException("Cannot merge a region onto itself"); ++ } ++ if (!this.isDead()) { ++ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); ++ } else if (mergeTarget.isDead()) { ++ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); ++ } ++ ++ for (final ThreadedRegionSection section : this.sectionByKey.values()) { ++ section.setRegionRelease(null); ++ mergeTarget.addSection(section); ++ } ++ for (final ThreadedRegionSection deadSection : this.deadSections) { ++ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { ++ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); ++ } ++ if (!mergeTarget.deadSections.add(deadSection)) { ++ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); ++ } ++ } ++ ++ // forward merge expectations ++ for (final ThreadedRegion region : this.expectingMergeFrom) { ++ if (!region.mergeIntoLater.remove(this)) { ++ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); ++ } ++ if (region != mergeTarget) { ++ region.mergeIntoLater(mergeTarget); ++ } ++ } ++ ++ // forward merge into ++ for (final ThreadedRegion region : this.mergeIntoLater) { ++ if (!region.expectingMergeFrom.remove(this)) { ++ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); ++ } ++ if (region != mergeTarget) { ++ mergeTarget.mergeIntoLater(region); ++ } ++ } ++ ++ // finally, merge data ++ if (this.data != null) { ++ this.data.mergeInto(mergeTarget); ++ } ++ } ++ ++ private void mergeIntoLater(final ThreadedRegion region) { ++ if (region.isDead()) { ++ throw new IllegalStateException("Trying to merge later into a dead region: " + region); ++ } ++ final boolean add1, add2; ++ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { ++ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); ++ } ++ } ++ ++ private boolean tryKill() { ++ switch (this.state) { ++ case STATE_NOT_READY: { ++ this.state = STATE_DEAD; ++ this.onRemove(false); ++ return true; ++ } ++ case STATE_STEADY_STATE: { ++ this.state = STATE_DEAD; ++ this.onRemove(true); ++ return true; ++ } ++ case STATE_TICKING: { ++ return false; ++ } ++ case STATE_DEAD: { ++ throw new IllegalStateException("Already dead"); ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + this.state); ++ } ++ } ++ } ++ ++ private boolean isDead() { ++ return this.state == STATE_DEAD; ++ } ++ ++ private boolean isTicking() { ++ return this.state == STATE_TICKING; ++ } ++ ++ private void removeDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.remove(section); ++ } ++ ++ private void addDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.add(section); ++ } ++ ++ private void addSection(final ThreadedRegionSection section) { ++ if (section.getRegionPlain() != null) { ++ throw new IllegalStateException("Section already has region"); ++ } ++ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { ++ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); ++ } ++ section.setRegionRelease(this); ++ } ++ ++ public R getData() { ++ return this.data; ++ } ++ ++ public boolean tryMarkTicking() { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_STEADY_STATE) { ++ return false; ++ } ++ ++ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + this + " should not be steady state"); ++ } ++ ++ this.state = STATE_TICKING; ++ return true; ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ public boolean markNotTicking() { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_TICKING) { ++ throw new IllegalStateException("Attempting to release non-locked state"); ++ } ++ ++ this.regioniser.onRegionRelease(this); ++ ++ return this.state == STATE_STEADY_STATE; ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(128); ++ ++ ret.append("ThreadedRegion{"); ++ ret.append("state=").append(this.state).append(','); ++ // To avoid recursion in toString, maybe fix later? ++ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); ++ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); ++ ++ ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); ++ ret.append("sections=["); ++ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { ++ final ThreadedRegionSection section = iterator.next(); ++ ++ ret.append(section.toString()); ++ if (iterator.hasNext()) { ++ ret.append(','); ++ } ++ } ++ ret.append(']'); ++ ++ ret.append('}'); ++ return ret.toString(); ++ } ++ } ++ ++ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { ++ ++ public final int sectionX; ++ public final int sectionZ; ++ public final long sectionKey; ++ private final long[] chunksBitset; ++ private int chunkCount; ++ private int nonEmptyNeighbours; ++ ++ private ThreadedRegion region; ++ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); ++ ++ public final ThreadedRegioniser regioniser; ++ ++ private final int regionChunkShift; ++ private final int regionChunkMask; ++ ++ private final S data; ++ ++ private ThreadedRegion getRegionPlain() { ++ return (ThreadedRegion)REGION_HANDLE.get(this); ++ } ++ ++ private ThreadedRegion getRegionAcquire() { ++ return (ThreadedRegion)REGION_HANDLE.getAcquire(this); ++ } ++ ++ private void setRegionRelease(final ThreadedRegion value) { ++ REGION_HANDLE.setRelease(this, value); ++ } ++ ++ // creates an empty section with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegioniser regioniser) { ++ this.sectionX = sectionX; ++ this.sectionZ = sectionZ; ++ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; ++ this.regioniser = regioniser; ++ this.regionChunkShift = regioniser.sectionChunkShift; ++ this.regionChunkMask = regioniser.regionSectionChunkSize - 1; ++ this.data = regioniser.callbacks ++ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); ++ } ++ ++ // creates a section with an initial chunk with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegioniser regioniser, ++ final int chunkXInit, final int chunkZInit) { ++ this(sectionX, sectionZ, regioniser); ++ ++ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); ++ this.chunkCount = 1; ++ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE ++ } ++ ++ // creates an empty section with the specified number of non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegioniser regioniser, ++ final int nonEmptyNeighbours) { ++ this(sectionX, sectionZ, regioniser); ++ ++ this.nonEmptyNeighbours = nonEmptyNeighbours; ++ } ++ ++ private boolean isEmpty() { ++ return this.chunkCount == 0; ++ } ++ ++ private boolean hasOnlyOneChunk() { ++ return this.chunkCount == 1; ++ } ++ ++ public boolean hasNonEmptyNeighbours() { ++ return this.nonEmptyNeighbours != 0; ++ } ++ ++ /** ++ * Returns the section data associated with this region section. May be {@code null}. ++ */ ++ public S getData() { ++ return this.data; ++ } ++ ++ /** ++ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. ++ */ ++ public ThreadedRegion getRegion() { ++ return this.getRegionAcquire(); ++ } ++ ++ private int getChunkIndex(final int chunkX, final int chunkZ) { ++ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); ++ } ++ ++ private void markAlive() { ++ this.getRegionPlain().removeDeadSection(this); ++ } ++ ++ private void markDead() { ++ this.getRegionPlain().addDeadSection(this); ++ } ++ ++ private void incrementNonEmptyNeighbours() { ++ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { ++ this.markAlive(); ++ } ++ final int createRadius = this.regioniser.emptySectionCreateRadius; ++ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { ++ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); ++ } ++ } ++ ++ private void decrementNonEmptyNeighbours() { ++ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { ++ this.markDead(); ++ } ++ if (this.nonEmptyNeighbours < 0) { ++ throw new IllegalStateException("Non empty neighbours reached zero"); ++ } ++ } ++ ++ /** ++ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create ++ * dead sections / increase non-empty neighbour count for neighbouring sections. ++ */ ++ private boolean addChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); ++ if (after == bitset) { ++ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean notEmpty = ++this.chunkCount == 1; ++ if (notEmpty && this.nonEmptyNeighbours == 0) { ++ this.markAlive(); ++ } ++ return notEmpty; ++ } ++ ++ /** ++ * Returns whether the chunk count is now zero. Effectively returns whether ++ * the caller needs to decrement the neighbour count for neighbouring sections. ++ */ ++ private boolean removeChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); ++ if (before == bitset) { ++ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean empty = --this.chunkCount == 0; ++ if (empty && this.nonEmptyNeighbours == 0) { ++ this.markDead(); ++ } ++ return empty; ++ } ++ ++ @Override ++ public String toString() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "hash=" + this.hashCode() + ++ "}"; ++ } ++ ++ public String toStringWithRegion() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "hash=" + this.hashCode() + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "region=" + this.getRegionAcquire() + ++ "}"; ++ } ++ ++ private static String toString(final long[] array) { ++ final StringBuilder ret = new StringBuilder(); ++ final char[] zeros = new char[Long.SIZE / 4]; ++ for (final long value : array) { ++ // zero pad the hex string ++ Arrays.fill(zeros, '0'); ++ final String string = Long.toHexString(value); ++ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); ++ ++ ret.append(zeros); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Splits this region data into the specified regions set. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param regioniser Regioniser for which the regions reside in. ++ * @param into A map of region section coordinate key to the region that owns the section. ++ * @param regions The set of regions to split into. ++ */ ++ public void split(final ThreadedRegioniser regioniser, final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions); ++ ++ /** ++ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined ++ * except that its region data is already created. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param into Specified region. ++ */ ++ public void mergeInto(final ThreadedRegion into); ++ } ++ ++ public static interface ThreadedRegionSectionData {} ++ ++ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Creates new section data for the specified section x and section z. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param sectionX x coordinate of the section. ++ * @param sectionZ z coordinate of the section. ++ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that ++ * produces a section coordinate. ++ * @return New section data, may be {@code null}. ++ */ ++ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); ++ ++ /** ++ * Creates new region data for the specified region. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param forRegion The region to create the data for. ++ * @return New region data, may be {@code null}. ++ */ ++ public R createNewData(final ThreadedRegion forRegion); ++ ++ /** ++ * Callback for when a region is created. This is invoked after the region is completely set up, ++ * so its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that was created. ++ */ ++ public void onRegionCreate(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so ++ * its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is about to be destroyed. ++ */ ++ public void onRegionDestroy(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is considered "active." An active region x is a non-destroyed region which ++ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are ++ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any ++ * other region considering the regioniser's empty section radius. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now active. ++ */ ++ public void onRegionActive(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but ++ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, ++ * an inactive region may not tick and needs to be merged into its neighbouring regions. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now inactive. ++ */ ++ public void onRegionInactive(final ThreadedRegion region); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickData.java b/src/main/java/io/papermc/paper/threadedregions/TickData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickData.java +@@ -0,0 +1,333 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import io.papermc.paper.util.IntervalledCounter; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++ ++public final class TickData { ++ ++ private final long interval; // ns ++ ++ private final ArrayDeque timeData = new ArrayDeque<>(); ++ ++ public TickData(final long intervalNS) { ++ this.interval = intervalNS; ++ } ++ ++ public void addDataFrom(final TickRegionScheduler.TickTime time) { ++ final long start = time.tickStart(); ++ ++ TickRegionScheduler.TickTime first; ++ while ((first = this.timeData.peekFirst()) != null) { ++ // only remove data completely out of window ++ if ((start - first.tickEnd()) <= this.interval) { ++ break; ++ } ++ this.timeData.pollFirst(); ++ } ++ ++ this.timeData.add(time); ++ } ++ ++ // fromIndex inclusive, toIndex exclusive ++ // will throw if arr.length == 0 ++ private static double median(final long[] arr, final int fromIndex, final int toIndex) { ++ final int len = toIndex - fromIndex; ++ final int middle = fromIndex + (len >>> 1); ++ if ((len & 1) == 0) { ++ // even, average the two middle points ++ return (double)(arr[middle - 1] + arr[middle]) / 2.0; ++ } else { ++ // odd, just grab the middle ++ return (double)arr[middle]; ++ } ++ } ++ ++ // will throw if arr.length == 0 ++ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, ++ final boolean inverse) { ++ final int len = toIndex - fromIndex; ++ long sum = 0L; ++ final double median = median(arr, fromIndex, toIndex); ++ long min = arr[0]; ++ long max = arr[0]; ++ ++ for (int i = fromIndex; i < toIndex; ++i) { ++ final long val = arr[i]; ++ sum += val; ++ if (val < min) { ++ min = val; ++ } ++ if (val > max) { ++ max = val; ++ } ++ } ++ ++ if (inverse) { ++ // for positive a,b we have that a >= b if and only if 1/a <= 1/b ++ return new SegmentData( ++ len, ++ (double)len / ((double)sum / 1.0E9), ++ 1.0E9 / median, ++ 1.0E9 / (double)max, ++ 1.0E9 / (double)min ++ ); ++ } else { ++ return new SegmentData( ++ len, ++ (double)sum / (double)len, ++ median, ++ (double)min, ++ (double)max ++ ); ++ } ++ } ++ ++ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, ++ final int percent99BestStart, final int percent99BestEnd, ++ final int percent95BestStart, final int percent95BestEnd, ++ final int percent1WorstStart, final int percent1WorstEnd, ++ final int percent5WorstStart, final int percent5WorstEnd, ++ final boolean inverse) { ++ return new SegmentedAverage( ++ computeSegmentData(data, allStart, allEnd, inverse), ++ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), ++ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), ++ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), ++ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) ++ ); ++ } ++ ++ private static record TickInformation( ++ long differenceFromLastTick, ++ long tickTime, ++ long tickTimeCPU ++ ) {} ++ ++ // rets null if there is no data ++ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { ++ if (this.timeData.isEmpty() && inProgress == null) { ++ return null; ++ } ++ ++ final List allData = new ArrayList<>(this.timeData); ++ if (inProgress != null) { ++ allData.add(inProgress); ++ } ++ ++ final long intervalStart = allData.get(0).tickStart(); ++ final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); ++ ++ // to make utilisation accurate, we need to take the total time used over the last interval period - ++ // this means if a tick start before the measurement interval, but ends within the interval, then we ++ // only consider the time it spent ticking inside the interval ++ long totalTimeOverInterval = 0L; ++ long measureStart = endTime - this.interval; ++ ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { ++ final long diff = time.tickEnd() - measureStart; ++ if (diff > 0L) { ++ totalTimeOverInterval += diff; ++ } // else: the time is entirely out of interval ++ } else { ++ totalTimeOverInterval += time.tickLength(); ++ } ++ } ++ ++ // we only care about ticks, but because of inbetween tick task execution ++ // there will be data in allData that isn't ticks. But, that data cannot ++ // be ignored since it contributes to utilisation. ++ // So, we will "compact" the data by merging any inbetween tick times ++ // the next tick. ++ // If there is no "next tick", then we will create one. ++ final List collapsedData = new ArrayList<>(); ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final List toCollapse = new ArrayList<>(); ++ TickRegionScheduler.TickTime lastTick = null; ++ for (;i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (!time.isTickExecution()) { ++ toCollapse.add(time); ++ continue; ++ } ++ lastTick = time; ++ break; ++ } ++ ++ if (toCollapse.isEmpty()) { ++ // nothing to collapse ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength(), ++ last.supportCPUTime() ? last.tickCpuTime() : 0L ++ ) ++ ); ++ } else { ++ long totalTickTime = 0L; ++ long totalCpuTime = 0L; ++ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { ++ final TickRegionScheduler.TickTime time = toCollapse.get(k); ++ totalTickTime += time.tickLength(); ++ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; ++ } ++ if (i < len) { ++ // we know there is a tick to collapse into ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength() + totalTickTime, ++ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime ++ ) ++ ); ++ } else { ++ // we do not have a tick to collapse into, so we must make one up ++ // we will assume that the tick is "starting now" and ongoing ++ ++ // compute difference between imaginary tick and last tick ++ final long differenceBetweenTicks; ++ if (lastTick != null) { ++ // we have a last tick, use it ++ differenceBetweenTicks = lastTick.tickStart(); ++ } else { ++ // we don't have a last tick, so we must make one up that makes sense ++ // if the current interval exceeds the max tick time, then use it ++ ++ // Otherwise use the interval length. ++ // This is how differenceFromLastTick() works on TickTime when there is no previous interval. ++ differenceBetweenTicks = Math.max( ++ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime ++ ); ++ } ++ ++ collapsedData.add( ++ new TickInformation( ++ differenceBetweenTicks, ++ totalTickTime, ++ totalCpuTime ++ ) ++ ); ++ } ++ } ++ } ++ ++ ++ final int collectedTicks = collapsedData.size(); ++ final long[] tickStartToStartDifferences = new long[collectedTicks]; ++ final long[] timePerTickDataRaw = new long[collectedTicks]; ++ final long[] missingCPUTimeDataRaw = new long[collectedTicks]; ++ ++ long totalTimeTicking = 0L; ++ ++ int i = 0; ++ for (final TickInformation time : collapsedData) { ++ tickStartToStartDifferences[i] = time.differenceFromLastTick(); ++ final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); ++ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); ++ ++ ++i; ++ ++ totalTimeTicking += timePerTick; ++ } ++ ++ Arrays.sort(tickStartToStartDifferences); ++ Arrays.sort(timePerTickDataRaw); ++ Arrays.sort(missingCPUTimeDataRaw); ++ ++ // Note: computeSegmentData cannot take start == end ++ final int allStart = 0; ++ final int allEnd = collectedTicks; ++ final int percent95BestStart = 0; ++ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); ++ final int percent99BestStart = 0; ++ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end ++ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); ++ final int percent1WorstStart = (int)(0.99 * collectedTicks); ++ final int percent1WorstEnd = collectedTicks; ++ final int percent5WorstStart = (int)(0.95 * collectedTicks); ++ final int percent5WorstEnd = collectedTicks; ++ ++ final SegmentedAverage tpsData = computeSegmentedAverage( ++ tickStartToStartDifferences, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ true ++ ); ++ ++ final SegmentedAverage timePerTickData = computeSegmentedAverage( ++ timePerTickDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( ++ missingCPUTimeDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final double utilisation = (double)totalTimeOverInterval / (double)this.interval; ++ ++ return new TickReportData( ++ collectedTicks, ++ intervalStart, ++ intervalEnd, ++ totalTimeTicking, ++ utilisation, ++ ++ tpsData, ++ timePerTickData, ++ missingCPUTimeData ++ ); ++ } ++ ++ public static final record TickReportData( ++ int collectedTicks, ++ long collectedTickIntervalStart, ++ long collectedTickIntervalEnd, ++ long totalTimeTicking, ++ double utilisation, ++ ++ SegmentedAverage tpsData, ++ // in ns ++ SegmentedAverage timePerTickData, ++ // in ns ++ SegmentedAverage missingCPUTimeData ++ ) {} ++ ++ public static final record SegmentedAverage( ++ SegmentData segmentAll, ++ SegmentData segment99PercentBest, ++ SegmentData segment95PercentBest, ++ SegmentData segment5PercentWorst, ++ SegmentData segment1PercentWorst ++ ) {} ++ ++ public static final record SegmentData( ++ int count, ++ double average, ++ double median, ++ double least, ++ double greatest ++ ) {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..65145994bd062c5c22d8fdf8124e7833323a3ff2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -0,0 +1,544 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.TickThread; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.lang.management.ManagementFactory; ++import java.lang.management.ThreadMXBean; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegionScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); ++ private static final boolean MEASURE_CPU_TIME; ++ static { ++ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); ++ if (MEASURE_CPU_TIME) { ++ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); ++ } else { ++ LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); ++ } ++ } ++ ++ public static final int TICK_RATE = 20; ++ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns ++ ++ private final SchedulerThreadPool scheduler; ++ ++ public TickRegionScheduler(final int threads) { ++ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); ++ return ret; ++ } ++ }); ++ } ++ ++ public int getTotalThreadCount() { ++ return this.scheduler.getThreads().length; ++ } ++ ++ private static void setTickingRegion(final ThreadedRegioniser.ThreadedRegion region) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (region != null && tickThreadRunner.currentTickingRegion != null) { ++ throw new IllegalStateException("Trying to double set ticking region!"); ++ } ++ if (region == null && tickThreadRunner.currentTickingRegion == null) { ++ throw new IllegalStateException("Trying to double unset ticking region!"); ++ } ++ tickThreadRunner.currentTickingRegion = region; ++ if (region != null) { ++ tickThreadRunner.currentTickingWorldRegionisedData = region.regioniser.world.worldRegionData.get(); ++ } else { ++ tickThreadRunner.currentTickingWorldRegionisedData = null; ++ } ++ } ++ ++ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (task != null && tickThreadRunner.currentTickingTask != null) { ++ throw new IllegalStateException("Trying to double set ticking task!"); ++ } ++ if (task == null && tickThreadRunner.currentTickingTask == null) { ++ throw new IllegalStateException("Trying to double unset ticking task!"); ++ } ++ tickThreadRunner.currentTickingTask = task; ++ } ++ ++ /** ++ * Returns the current ticking region, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static ThreadedRegioniser.ThreadedRegion getCurrentRegion() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getRegion(); ++ } ++ return tickThreadRunner.currentTickingRegion; ++ } ++ ++ /** ++ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. ++ * This is a faster alternative to calling the {@link RegionisedData#get()} method. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static RegionisedWorldData getCurrentRegionisedWorldData() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getWorldData(); ++ } ++ return tickThreadRunner.currentTickingWorldRegionisedData; ++ } ++ ++ /** ++ * Returns the current ticking task, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return null; ++ } ++ return tickThreadRunner.currentTickingTask; ++ } ++ ++ /** ++ * Schedules the given region ++ * @throws IllegalStateException If the region is already scheduled or is ticking ++ */ ++ public void scheduleRegion(final RegionScheduleHandle region) { ++ region.scheduler = this; ++ this.scheduler.schedule(region); ++ } ++ ++ /** ++ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task ++ * execution, then it will be cancelled after. ++ */ ++ public void descheduleRegion(final RegionScheduleHandle region) { ++ // To avoid acquiring any of the locks the scheduler may be using, we ++ // simply cancel the next action. ++ region.markNonSchedulable(); ++ } ++ ++ /** ++ * Updates the tick start to the farthest into the future of its current scheduled time and the ++ * provided time. ++ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its ++ * current start time, {@code true} if the next tick start was adjusted. ++ */ ++ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { ++ return this.scheduler.updateTickStartToMax(region, newStart); ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ return this.scheduler.halt(sync, maxWaitNS); ++ } ++ ++ public void setHasTasks(final RegionScheduleHandle region) { ++ this.scheduler.notifyTasks(region); ++ } ++ ++ public void init() { ++ this.scheduler.start(); ++ } ++ ++ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { ++ // when a region fails, we need to shut down the server gracefully ++ ++ // prevent further ticks from occurring ++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD ++ this.scheduler.halt(false, 0L); ++ ++ final ChunkPos center = handle.region.region.getCenterChunk(); ++ ++ LOGGER.error("Region #" + handle.region.id + " centered at chunk " + center + " failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); ++ ++ MinecraftServer.getServer().stopServer(); ++ } ++ ++ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. ++ // This is much faster than a thread local, since the thread local has to use a map lookup. ++ private static final class TickThreadRunner extends TickThread { ++ ++ private ThreadedRegioniser.ThreadedRegion currentTickingRegion; ++ private RegionisedWorldData currentTickingWorldRegionisedData; ++ private SchedulerThreadPool.SchedulableTick currentTickingTask; ++ ++ public TickThreadRunner(final Runnable run, final String name) { ++ super(run, name); ++ } ++ } ++ ++ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { ++ ++ protected long currentTick; ++ protected long lastTickStart; ++ ++ protected final TickData tickTimes5s; ++ protected final TickData tickTimes15s; ++ protected final TickData tickTimes1m; ++ protected final TickData tickTimes5m; ++ protected final TickData tickTimes15m; ++ protected TickTime currentTickData; ++ protected Thread currentTickingThread; ++ ++ public final TickRegions.TickRegionData region; ++ private final AtomicBoolean cancelled = new AtomicBoolean(); ++ ++ protected final Schedule tickSchedule; ++ ++ private TickRegionScheduler scheduler; ++ ++ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { ++ this.currentTick = 0L; ++ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; ++ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); ++ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); ++ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); ++ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); ++ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); ++ this.region = region; ++ ++ this.setScheduledStart(firstStart); ++ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); ++ } ++ ++ /** ++ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} ++ * so that the tick schedule and scheduled start remain synchronised ++ */ ++ protected final void updateScheduledStart(final long to) { ++ this.setScheduledStart(to); ++ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); ++ } ++ ++ public final void markNonSchedulable() { ++ this.cancelled.set(true); ++ } ++ ++ protected abstract boolean tryMarkTicking(); ++ ++ protected abstract boolean markNotTicking(); ++ ++ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); ++ ++ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); ++ ++ protected abstract boolean hasIntermediateTasks(); ++ ++ @Override ++ public final boolean hasTasks() { ++ return this.hasIntermediateTasks(); ++ } ++ ++ @Override ++ public final Boolean runTasks(final BooleanSupplier canContinue) { ++ if (this.cancelled.get()) { ++ return null; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return null; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ false ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ final boolean ret; ++ try { ++ ret = this.runRegionTasks(() -> { ++ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); ++ }); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, true, thr); ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // don't release region for another tick ++ return null; ++ } finally { ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ final TickTime time = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, ++ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false ++ ); ++ ++ this.addTickTime(time); ++ } ++ ++ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); ++ } ++ ++ @Override ++ public final boolean runTick() { ++ // Remember, we are supposed use setScheduledStart if we return true here, otherwise ++ // the scheduler will try to schedule for the same time. ++ if (this.cancelled.get()) { ++ return false; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ // use max(), don't assume that tickStart >= scheduledStart ++ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return false; ++ } ++ if (this.cancelled.get()) { ++ this.markNotTicking(); ++ // region should be killed ++ return false; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ this.incrementTickCount(); ++ final long lastTickStart = this.lastTickStart; ++ this.lastTickStart = tickStart; ++ ++ final long scheduledStart = this.getScheduledStart(); ++ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ true ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ try { ++ // next start isn't updated until the end of this tick ++ this.tickRegion(tickCount, tickStart, scheduledEnd); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, false, thr); ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further ++ return false; ++ } finally { ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions ++ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest ++ // of the current time and "ideal" next tick start. ++ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); ++ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); ++ ++ final TickTime time = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true ++ ); ++ ++ this.addTickTime(time); ++ } ++ ++ // Only AFTER updating the tickStart ++ return this.markNotTicking() && !this.cancelled.get(); ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ private void addTickTime(final TickTime time) { ++ synchronized (this) { ++ this.currentTickData = null; ++ this.currentTickingThread = null; ++ this.tickTimes5s.addDataFrom(time); ++ this.tickTimes15s.addDataFrom(time); ++ this.tickTimes1m.addDataFrom(time); ++ this.tickTimes5m.addDataFrom(time); ++ this.tickTimes15m.addDataFrom(time); ++ } ++ } ++ ++ private TickTime adjustCurrentTickData(final long tickEnd) { ++ final TickTime currentTickData = this.currentTickData; ++ if (currentTickData == null) { ++ return null; ++ } ++ ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; ++ ++ return new TickTime( ++ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), ++ currentTickData.tickStart(), currentTickData.tickStartCPU(), ++ tickEnd, cpuEnd, ++ MEASURE_CPU_TIME, currentTickData.isTickExecution() ++ ); ++ } ++ ++ public final TickData.TickReportData getTickReport5s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport1m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport5m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ private void incrementTickCount() { ++ ++this.currentTick; ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ public final long getCurrentTick() { ++ return this.currentTick; ++ } ++ ++ protected final void setCurrentTick(final long value) { ++ this.currentTick = value; ++ } ++ } ++ ++ // All time units are in nanoseconds. ++ public static final record TickTime( ++ long previousTickStart, ++ long scheduledTickStart, ++ long tickStart, ++ long tickStartCPU, ++ long tickEnd, ++ long tickEndCPU, ++ boolean supportCPUTime, ++ boolean isTickExecution ++ ) { ++ /** ++ * The difference between the start tick time and the scheduled start tick time. This value is ++ * < 0 if the tick started before the scheduled tick time. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long startOvershoot() { ++ return this.tickStart - this.scheduledTickStart; ++ } ++ ++ /** ++ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). ++ */ ++ public final long tickLength() { ++ return this.tickEnd - this.tickStart; ++ } ++ ++ /** ++ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, ++ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE ++ * if CPU time measurement is not supported. ++ */ ++ public final long tickCpuTime() { ++ if (!this.supportCPUTime()) { ++ return Long.MIN_VALUE; ++ } ++ return this.tickEndCPU - this.tickStartCPU; ++ } ++ ++ /** ++ * The difference in time from the start of the last tick to the start of the current tick. If there is no ++ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long differenceFromLastTick() { ++ if (this.hasLastTick()) { ++ return this.tickStart - this.previousTickStart; ++ } ++ return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); ++ } ++ ++ /** ++ * Returns whether there was a tick that occurred before this one. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public boolean hasLastTick() { ++ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; ++ } ++ ++ /* ++ * Remember, this is the expected behavior of the following: ++ * ++ * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). ++ * ++ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). ++ */ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c17669c1e98cd954643fa3b988c12b4b6c3b174e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +@@ -0,0 +1,340 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import java.util.Iterator; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegions implements ThreadedRegioniser.RegionCallbacks { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static int getRegionChunkShift() { ++ return 4; ++ } ++ ++ private static boolean initialised; ++ private static TickRegionScheduler scheduler; ++ ++ public static TickRegionScheduler getScheduler() { ++ return scheduler; ++ } ++ ++ public static void init(final GlobalConfiguration.ThreadedRegions config) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ ++ int tickThreads; ++ if (config.threads <= 0) { ++ tickThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (tickThreads <= 4) { ++ tickThreads = 1; ++ } else { ++ tickThreads = (2 * tickThreads) / 3; ++ } ++ } else { ++ tickThreads = config.threads; ++ } ++ ++ scheduler = new TickRegionScheduler(tickThreads); ++ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); ++ } ++ ++ @Override ++ public TickRegionData createNewData(final ThreadedRegioniser.ThreadedRegion region) { ++ return new TickRegionData(region); ++ } ++ ++ @Override ++ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { ++ return null; ++ } ++ ++ @Override ++ public void onRegionCreate(final ThreadedRegioniser.ThreadedRegion region) { ++ // nothing for now ++ } ++ ++ @Override ++ public void onRegionDestroy(final ThreadedRegioniser.ThreadedRegion region) { ++ // nothing for now ++ } ++ ++ @Override ++ public void onRegionActive(final ThreadedRegioniser.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ data.tickHandle.checkInitialSchedule(); ++ scheduler.scheduleRegion(data.tickHandle); ++ } ++ ++ @Override ++ public void onRegionInactive(final ThreadedRegioniser.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ scheduler.descheduleRegion(data.tickHandle); ++ // old handle cannot be scheduled anymore, copy to a new handle ++ data.tickHandle = data.tickHandle.copy(); ++ } ++ ++ public static final class TickRegionSectionData implements ThreadedRegioniser.ThreadedRegionSectionData {} ++ ++ public static final class TickRegionData implements ThreadedRegioniser.ThreadedRegionData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ /** Never 0L, since 0L is reserved for global region. */ ++ public final long id = ID_GENERATOR.incrementAndGet(); ++ ++ public final ThreadedRegioniser.ThreadedRegion region; ++ public final ServerLevel world; ++ ++ // generic regionised data ++ private final Reference2ReferenceOpenHashMap, Object> regionisedData = new Reference2ReferenceOpenHashMap<>(); ++ ++ // tick data ++ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); ++ ++ // queue data ++ private final RegionisedTaskQueue.RegionTaskQueueData taskQueueData; ++ ++ // chunk holder manager data ++ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); ++ ++ private TickRegionData(final ThreadedRegioniser.ThreadedRegion region) { ++ this.region = region; ++ this.world = region.regioniser.world; ++ this.taskQueueData = new RegionisedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); ++ } ++ ++ public RegionisedTaskQueue.RegionTaskQueueData getTaskQueueData() { ++ return this.taskQueueData; ++ } ++ ++ // the value returned can be invalidated at any time, except when the caller ++ // is ticking this region ++ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { ++ return this.tickHandle; ++ } ++ ++ public long getCurrentTick() { ++ return this.tickHandle.getCurrentTick(); ++ } ++ ++ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { ++ return this.holderManagerRegionData; ++ } ++ ++ T getOrCreateRegionisedData(final RegionisedData regionisedData) { ++ T ret = (T)this.regionisedData.get(regionisedData); ++ ++ if (ret != null) { ++ return ret; ++ } ++ ++ ret = regionisedData.createNewValue(); ++ this.regionisedData.put(regionisedData, ret); ++ ++ return ret; ++ } ++ ++ @Override ++ public void split(final ThreadedRegioniser regioniser, ++ final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions) { ++ final int shift = regioniser.sectionChunkShift; ++ ++ // tick data ++ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), ++ // and the other regions to split into are not scheduled yet. ++ for (final ThreadedRegioniser.ThreadedRegion region : regions) { ++ final TickRegionData data = region.getData(); ++ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); ++ } ++ ++ // generic regionised data ++ for (final Iterator, Object>> dataIterator = this.regionisedData.reference2ReferenceEntrySet().fastIterator(); ++ dataIterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); ++ final RegionisedData data = regionDataEntry.getKey(); ++ final Object from = regionDataEntry.getValue(); ++ ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegioniser.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().getOrCreateRegionisedData(data)); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegioniser.ThreadedRegion region = entry.getValue(); ++ final Object to = region.getData().getOrCreateRegionisedData(data); ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ ((RegionisedData)data).getCallback().split(from, shift, regionToData, dataSet); ++ } ++ ++ // chunk holder manager data ++ { ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegioniser.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().holderManagerRegionData); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegioniser.ThreadedRegion region = entry.getValue(); ++ final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ this.holderManagerRegionData.split(shift, regionToData, dataSet); ++ } ++ ++ // task queue ++ this.taskQueueData.split(regioniser, into); ++ } ++ ++ @Override ++ public void mergeInto(final ThreadedRegioniser.ThreadedRegion into) { ++ // Note: merge target is always a region being released from ticking ++ final TickRegionData data = into.getData(); ++ final long currentTickTo = data.getCurrentTick(); ++ final long currentTickFrom = this.getCurrentTick(); ++ ++ // here we can access tickHandle because the target (into) is the region being released, so it is ++ // not actually scheduled ++ // there's not really a great solution to the tick problem, no matter what it'll be messed up ++ // we will pick the greatest time delay so that tps will not exceed TICK_RATE ++ data.tickHandle.updateSchedulingToMax(this.tickHandle); ++ ++ // generic regionised data ++ final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd ++ for (final Iterator, Object>> iterator = this.regionisedData.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); ++ final RegionisedData regionisedData = entry.getKey(); ++ final Object from = entry.getValue(); ++ final Object to = into.getData().getOrCreateRegionisedData(regionisedData); ++ ++ ((RegionisedData)regionisedData).getCallback().merge(from, to, fromTickOffset); ++ } ++ ++ // chunk holder manager data ++ this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); ++ ++ // task queue ++ this.taskQueueData.mergeInto(data.taskQueueData); ++ } ++ } ++ ++ private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final TickRegionData region; ++ ++ private ConcreteRegionTickHandle(final TickRegionData region, final long start) { ++ super(region, start); ++ this.region = region; ++ } ++ ++ private ConcreteRegionTickHandle copy() { ++ final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); ++ ++ ret.currentTick = this.currentTick; ++ ret.lastTickStart = this.lastTickStart; ++ ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); ++ ++ return ret; ++ } ++ ++ private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(from.getScheduledStart()); ++ return; ++ } ++ ++ this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); ++ } ++ ++ private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { ++ this.currentTick = from.currentTick; ++ ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); ++ this.setScheduledStart(from.getScheduledStart()); ++ } ++ ++ private void checkInitialSchedule() { ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ } ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return this.region.region.tryMarkTicking(); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.region.region.markNotTicking(); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ final RegionisedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; ++ boolean executeChunkTask = true; ++ boolean executeTickTask = true; ++ do { ++ if (executeTickTask) { ++ executeTickTask = queue.executeTickTask(); ++ } ++ if (executeChunkTask) { ++ executeChunkTask = queue.executeChunkTask(); ++ } ++ } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return this.region.taskQueueData.hasTasks(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ee4e3c42abc4e62e4eaee1af9e3cdad27765b39d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +@@ -0,0 +1,331 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import io.papermc.paper.threadedregions.ThreadedRegioniser; ++import io.papermc.paper.threadedregions.TickData; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.Bukkit; ++import org.bukkit.World; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.Player; ++import java.text.DecimalFormat; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Locale; ++import java.util.concurrent.TimeUnit; ++ ++public final class CommandServerHealth extends Command { ++ ++ private static final DecimalFormat TWO_DECIMAL_PLACES = new DecimalFormat("#0.00"); ++ private static final DecimalFormat ONE_DECIMAL_PLACES = new DecimalFormat("#0.0"); ++ ++ private static final TextColor HEADER = TextColor.color(79, 164, 240); ++ private static final TextColor PRIMARY = TextColor.color(48, 145, 237); ++ private static final TextColor SECONDARY = TextColor.color(104, 177, 240); ++ private static final TextColor INFORMATION = TextColor.color(145, 198, 243); ++ private static final TextColor LIST = TextColor.color(33, 97, 188); ++ ++ public CommandServerHealth() { ++ super("tps"); ++ this.setUsage("/ [server/region] [lowest regions to display]"); ++ this.setDescription("Reports information about server health."); ++ this.setPermission("bukkit.command.tps"); ++ } ++ ++ @Override ++ public boolean testPermissionSilent(final CommandSender target) { ++ // TODO for now ++ return true; ++ } ++ ++ private static Component formatRegionInfo(final String prefix, final TextColor primary, final double util, final double mspt, final double tps, ++ final boolean newline) { ++ return Component.text() ++ .append(Component.text(prefix, primary, TextDecoration.BOLD)) ++ .append(Component.text(ONE_DECIMAL_PLACES.format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", primary)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", primary)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS" + (newline ? "\n" : ""), primary)) ++ .build(); ++ } ++ ++ private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); ++ return true; ++ } ++ ++ final long currTime = System.nanoTime(); ++ ++ final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ ++ final double util15s = report15s.utilisation(); ++ final double tps15s = report15s.tpsData().segmentAll().average(); ++ final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final double util1m = report1m.utilisation(); ++ final double tps1m = report1m.tpsData().segmentAll().average(); ++ final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ ++ final Component line = Component.text() ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append( ++ formatRegionInfo("15s: ", PRIMARY, util15s, mspt15s, tps15s, true) ++ ) ++ .append( ++ formatRegionInfo("1m: ", PRIMARY, util1m, mspt1m, tps1m, false) ++ ) ++ ++ .build(); ++ ++ sender.sendMessage(line); ++ ++ return true; ++ } ++ ++ private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { ++ final int lowestRegionsCount; ++ if (args.length < 2) { ++ lowestRegionsCount = 3; ++ } else { ++ try { ++ lowestRegionsCount = Integer.parseInt(args[1]); ++ } catch (final NumberFormatException ex) { ++ sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ ++ final List> regions = ++ new ArrayList<>(); ++ ++ for (final World bukkitWorld : Bukkit.getWorlds()) { ++ final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); ++ world.regioniser.computeForAllRegions(regions::add); ++ } ++ ++ final long currTime = System.nanoTime(); ++ ++ final double minTps; ++ final double medianTps; ++ final double maxTps; ++ long totalTime = 0; ++ double totalUtil = 0.0; ++ ++ final DoubleArrayList tpsByRegion = new DoubleArrayList(); ++ final List reportsByRegion = new ArrayList<>(); ++ ++ final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); ++ ++ for (final ThreadedRegioniser.ThreadedRegion region : regions) { ++ final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); ++ reportsByRegion.add(report); ++ totalUtil += (report == null ? 0.0 : report.utilisation()); ++ } ++ ++ tpsByRegion.sort(null); ++ if (!tpsByRegion.isEmpty()) { ++ minTps = tpsByRegion.getDouble(0); ++ maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); ++ ++ final int middle = tpsByRegion.size() >> 1; ++ if ((tpsByRegion.size() & 1) == 0) { ++ // even, average the two middle points ++ medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; ++ } else { ++ // odd, can just grab middle ++ medianTps = tpsByRegion.getDouble(middle); ++ } ++ } else { ++ // no regions = green ++ minTps = medianTps = maxTps = 20.0; ++ } ++ ++ final List, TickData.TickReportData>> ++ regionsBelowThreshold = new ArrayList<>(); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ final TickData.TickReportData report = reportsByRegion.get(i); ++ ++ regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); ++ } ++ ++ regionsBelowThreshold.sort((p1, p2) -> { ++ final TickData.TickReportData report1 = p1.right(); ++ final TickData.TickReportData report2 = p2.right(); ++ final double util1 = report1 == null ? 0.0 : report1.utilisation(); ++ final double util2 = report2 == null ? 0.0 : report2.utilisation(); ++ ++ // we want the largest first ++ return Double.compare(util2, util1); ++ }); ++ ++ final TextComponent.Builder lowestRegionsBuilder = Component.text(); ++ ++ if (sender instanceof Player) { ++ lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); ++ } ++ for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { ++ final ObjectObjectImmutablePair, TickData.TickReportData> ++ pair = regionsBelowThreshold.get(i); ++ ++ final TickData.TickReportData report = pair.right(); ++ final ThreadedRegioniser.ThreadedRegion region = ++ pair.left(); ++ ++ if (report == null) { ++ // skip regions with no data ++ continue; ++ } ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ final double util = report.utilisation(); ++ final double tps = report.tpsData().segmentAll().average(); ++ final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ final Component line = Component.text() ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append(Component.text(" ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS" + ((i + 1) == len ? "" : "\n"), PRIMARY)) ++ .build() ++ ++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) ++ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); ++ ++ lowestRegionsBuilder.append(line); ++ } ++ ++ sender.sendMessage( ++ Component.text() ++ .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Online Players: ", PRIMARY)) ++ .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Total regions: ", PRIMARY)) ++ .append(Component.text(regions.size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Utilisation: ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) ++ .append(Component.text("% / ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.format(maxThreadCount * 100.0), INFORMATION)) ++ .append(Component.text("%\n", PRIMARY)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Lowest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) ++ ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Median Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Highest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) ++ ++ .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) ++ .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) ++ .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(lowestRegionsBuilder.build()) ++ .build() ++ ); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ final String type; ++ if (args.length < 1) { ++ type = "server"; ++ } else { ++ type = args[0]; ++ } ++ ++ switch (type.toLowerCase(Locale.ROOT)) { ++ case "server": { ++ return executeServer(sender, commandLabel, args); ++ } ++ case "region": { ++ if (!(sender instanceof Entity)) { ++ sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); ++ return true; ++ } ++ return executeRegion(sender, commandLabel, args); ++ } ++ default: { ++ sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { ++ if (args.length == 0) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region")); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server")); ++ } ++ } else if (args.length == 1) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); ++ } ++ } ++ return new ArrayList<>(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a222ee74d64c1d1fa62f6fa91ce7b8f5cdeda2f9 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java +@@ -0,0 +1,106 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.util.HSVLike; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Function; ++ ++public final class CommandUtil { ++ ++ public static List getSortedList(final Iterable iterable) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ ret.add(val); ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ if (val.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(val); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ ret.add(transform.apply(val)); ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ final String string = transform.apply(val); ++ if (string.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(string); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static TextColor getColourForTPS(final double tps) { ++ final double difference = Math.min(Math.abs(20.0 - tps), 20.0); ++ final double coordinate; ++ if (difference <= 2.0) { ++ // >= 18 tps ++ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); ++ } else if (difference <= 5.0) { ++ // >= 15 tps ++ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); ++ } else if (difference <= 10.0) { ++ // >= 10 tps ++ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); ++ } else { ++ // >= 0.0 tps ++ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getColourForMSPT(final double mspt) { ++ final double clamped = Math.min(Math.abs(mspt), 50.0); ++ final double coordinate; ++ if (clamped <= 15.0) { ++ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); ++ } else if (clamped <= 25.0) { ++ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); ++ } else if (clamped <= 35.0) { ++ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); ++ } else if (clamped <= 40.0) { ++ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); ++ } else { ++ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getUtilisationColourRegion(final double util) { ++ // TODO anything better? ++ // assume 20TPS ++ return getColourForMSPT(util * 50.0); ++ } ++ ++ private CommandUtil() {} ++} +diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPA.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPA.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f220176831ff7030f00620b020fef9e0054f1d98 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPA.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerPlayer; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Function; ++ ++public class CommandsTPA extends Command { ++ ++ public CommandsTPA() { ++ super("tpa"); ++ } ++ ++ @Override ++ public boolean testPermission(final CommandSender target) { ++ return true; ++ } ++ ++ @Override ++ public boolean testPermissionSilent(final CommandSender target) { ++ return true; ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ if (!(sender instanceof CraftPlayer)) { ++ sender.sendMessage("TPA only works for players"); ++ return true; ++ } ++ ++ if (args.length != 1) { ++ sender.sendMessage("Must provide player target"); ++ return true; ++ } ++ ++ ++ ++ return true; ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String alias, ++ final String[] args) throws IllegalArgumentException { ++ if (!(sender instanceof CraftPlayer)) { ++ return new ArrayList<>(); ++ } ++ ++ final List players = MinecraftServer.getServer().getPlayerList().players; ++ ++ final Function playerToName = (final ServerPlayer value) -> { ++ return value.getGameProfile().getName(); ++ }; ++ ++ if (args.length == 0) { ++ return CommandUtil.getSortedList(players, playerToName); ++ } ++ ++ return CommandUtil.getSortedList(players, playerToName, args[0]); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java +index e08f4e39db4ee3fed62e37364d17dcc5c5683504..d849ea4ad40b3bac3d1add263c1cd4294b9bb274 100644 +--- a/src/main/java/io/papermc/paper/util/CachedLists.java ++++ b/src/main/java/io/papermc/paper/util/CachedLists.java +@@ -9,49 +9,57 @@ import java.util.List; + public final class CachedLists { + + // Paper start - optimise collisions +- static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); +- static boolean tempCollisionListInUse; ++ // Paper - region threading + + public static UnsafeList getTempCollisionList() { +- if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ if (worldData == null) { + return new UnsafeList<>(16); + } +- tempCollisionListInUse = true; +- return TEMP_COLLISION_LIST; ++ return worldData.tempCollisionList.get(); ++ // Paper end - region threading + } + + public static void returnTempCollisionList(List list) { +- if (list != TEMP_COLLISION_LIST) { ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ if (worldData == null) { + return; + } +- ((UnsafeList)list).setSize(0); +- tempCollisionListInUse = false; ++ worldData.tempCollisionList.ret(list); ++ // Paper end - region threading + } + +- static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); +- static boolean tempGetEntitiesListInUse; ++ // Paper - region threading + + public static UnsafeList getTempGetEntitiesList() { +- if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ if (worldData == null) { + return new UnsafeList<>(16); + } +- tempGetEntitiesListInUse = true; +- return TEMP_GET_ENTITIES_LIST; ++ return worldData.tempEntitiesList.get(); ++ // Paper end - region threading + } + + public static void returnTempGetEntitiesList(List list) { +- if (list != TEMP_GET_ENTITIES_LIST) { ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ if (worldData == null) { + return; + } +- ((UnsafeList)list).setSize(0); +- tempGetEntitiesListInUse = false; ++ worldData.tempEntitiesList.ret(list); ++ // Paper end - region threading + } + // Paper end - optimise collisions + + public static void reset() { +- // Paper start - optimise collisions +- TEMP_COLLISION_LIST.completeReset(); +- TEMP_GET_ENTITIES_LIST.completeReset(); +- // Paper end - optimise collisions ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ if (worldData != null) { ++ worldData.resetCollisionLists(); ++ } ++ // Paper end - region threading + } + } +diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java +index 413e4b6da027876dbbe8eb78f2568a440f431547..d29a4a3bab456df99fbccddc832a9ac2da880f31 100644 +--- a/src/main/java/io/papermc/paper/util/CoordinateUtils.java ++++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java +@@ -5,6 +5,7 @@ import net.minecraft.core.SectionPos; + import net.minecraft.util.Mth; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.phys.Vec3; + + public final class CoordinateUtils { + +@@ -122,6 +123,31 @@ public final class CoordinateUtils { + return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); + } + ++ // TODO rebase ++ public static int getBlockX(final Vec3 pos) { ++ return Mth.fastFloor(pos.x); ++ } ++ ++ public static int getBlockY(final Vec3 pos) { ++ return Mth.fastFloor(pos.y); ++ } ++ ++ public static int getBlockZ(final Vec3 pos) { ++ return Mth.fastFloor(pos.z); ++ } ++ ++ public static int getChunkX(final Vec3 pos) { ++ return Mth.fastFloor(pos.x) >> 4; ++ } ++ ++ public static int getChunkY(final Vec3 pos) { ++ return Mth.fastFloor(pos.y) >> 4; ++ } ++ ++ public static int getChunkZ(final Vec3 pos) { ++ return Mth.fastFloor(pos.z) >> 4; ++ } ++ + private CoordinateUtils() { + throw new RuntimeException(); + } +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index 6898c704e60d89d53c8ed114e5e12f73ed63605a..34f8a98f9e299a0c173f6b57bed54768251baaca 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -28,6 +28,7 @@ import net.minecraft.world.level.ClipContext; + import net.minecraft.world.level.Level; + import net.minecraft.world.level.chunk.ChunkAccess; + import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.phys.Vec3; + import org.apache.commons.lang.exception.ExceptionUtils; + import com.mojang.authlib.GameProfile; + import org.bukkit.Location; +@@ -332,6 +333,7 @@ public final class MCUtil { + */ + public static void ensureMain(String reason, Runnable run) { + if (!isMainThread()) { ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + if (reason != null) { + MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); + } +@@ -472,6 +474,30 @@ public final class MCUtil { + return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); + } + ++ // Paper start - TODO MERGE INTO MCUTIL ++ /** ++ * Converts a NMS World/Vector to Bukkit Location ++ * @param world ++ * @param pos ++ * @return ++ */ ++ public static Location toLocation(Level world, Vec3 pos) { ++ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z()); ++ } ++ ++ /** ++ * Converts a NMS World/Vector to Bukkit Location ++ * @param world ++ * @param pos ++ * @param yaw ++ * @param pitch ++ * @return ++ */ ++ public static Location toLocation(Level world, Vec3 pos, float yaw, float pitch) { ++ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); ++ } ++ // Paper end - TODO MERGE INTO MCUTIL ++ + /** + * Converts an NMS entity's current location to a Bukkit Location + * @param entity +diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +index fc57850b80303fcade89ca95794f63910404a407..7de0bd89b13dcb550cf78ceda625f5ab9f9f3599 100644 +--- a/src/main/java/io/papermc/paper/util/TickThread.java ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -1,8 +1,19 @@ + package io.papermc.paper.util; + ++import io.papermc.paper.threadedregions.RegionShutdownThread; ++import io.papermc.paper.threadedregions.RegionisedWorldData; ++import io.papermc.paper.threadedregions.ThreadedRegioniser; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++import net.minecraft.core.BlockPos; + import net.minecraft.server.MinecraftServer; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.util.Mth; + import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.phys.Vec3; + import org.bukkit.Bukkit; + import java.util.concurrent.atomic.AtomicInteger; + +@@ -38,6 +49,20 @@ public class TickThread extends Thread { + } + } + ++ public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -77,11 +102,75 @@ public class TickThread extends Thread { + return Thread.currentThread() instanceof TickThread; + } + ++ public static boolean isShutdownThread() { ++ return Thread.currentThread().getClass() == RegionShutdownThread.class; ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) { ++ return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) { ++ return isTickThreadFor(world, pos.x, pos.z); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) { ++ return isTickThreadFor(world, Mth.floor(pos.x) >> 4, Mth.floor(pos.z) >> 4); ++ } ++ + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { +- return Thread.currentThread() instanceof TickThread; ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ return isShutdownThread(); ++ } ++ return world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region; ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ return isShutdownThread(); ++ } ++ ++ final int minSectionX = (chunkX - radius) >> world.regioniser.sectionChunkShift; ++ final int maxSectionX = (chunkX + radius) >> world.regioniser.sectionChunkShift; ++ final int minSectionZ = (chunkZ - radius) >> world.regioniser.sectionChunkShift; ++ final int maxSectionZ = (chunkZ + radius) >> world.regioniser.sectionChunkShift; ++ ++ for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { ++ for (int secX = minSectionX; secX <= maxSectionX; ++secX) { ++ final int lowerLeftCX = secX << world.regioniser.sectionChunkShift; ++ final int lowerLeftCZ = secZ << world.regioniser.sectionChunkShift; ++ if (world.regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { ++ return false; ++ } ++ } ++ } ++ ++ return true; + } + + public static boolean isTickThreadFor(final Entity entity) { +- return Thread.currentThread() instanceof TickThread; ++ if (entity == null) { ++ return true; ++ } ++ final ThreadedRegioniser.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ return isShutdownThread(); ++ } ++ ++ final Level level = entity.level; ++ if (level != region.regioniser.world) { ++ // world mismatch ++ return false; ++ } ++ ++ final RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ ++ // pass through the check if the entity is removed and we own its chunk ++ return worldData.hasEntity(entity) || (entity.isRemoved() && !(entity instanceof ServerPlayer) && isTickThreadFor((ServerLevel)level, entity.chunkPosition())); + } + } +diff --git a/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cf9b66afc1762dbe2c625f09f9e804ca7dc0f128 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java +@@ -0,0 +1,273 @@ ++package io.papermc.paper.util.set; ++ ++import java.util.Comparator; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++ ++// TODO rebase into util patch ++public final class LinkedSortedSet implements Iterable { ++ ++ public final Comparator comparator; ++ ++ protected Link head; ++ protected Link tail; ++ ++ public LinkedSortedSet() { ++ this((Comparator)Comparator.naturalOrder()); ++ } ++ ++ public LinkedSortedSet(final Comparator comparator) { ++ this.comparator = comparator; ++ } ++ ++ public void clear() { ++ this.head = this.tail = null; ++ } ++ ++ public boolean isEmpty() { ++ return this.head == null; ++ } ++ ++ public E first() { ++ final Link head = this.head; ++ return head == null ? null : head.element; ++ } ++ ++ public E last() { ++ final Link tail = this.tail; ++ return tail == null ? null : tail.element; ++ } ++ ++ public boolean containsFirst(final E element) { ++ final Comparator comparator = this.comparator; ++ for (Link curr = this.head; curr != null; curr = curr.next) { ++ if (comparator.compare(element, curr.element) == 0) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public boolean containsLast(final E element) { ++ final Comparator comparator = this.comparator; ++ for (Link curr = this.tail; curr != null; curr = curr.prev) { ++ if (comparator.compare(element, curr.element) == 0) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ private void removeNode(final Link node) { ++ final Link prev = node.prev; ++ final Link next = node.next; ++ ++ // help GC ++ node.element = null; ++ node.prev = null; ++ node.next = null; ++ ++ if (prev == null) { ++ this.head = next; ++ } else { ++ prev.next = next; ++ } ++ ++ if (next == null) { ++ this.tail = prev; ++ } else { ++ next.prev = prev; ++ } ++ } ++ ++ public boolean remove(final Link link) { ++ if (link.element == null) { ++ return false; ++ } ++ ++ this.removeNode(link); ++ return true; ++ } ++ ++ public boolean removeFirst(final E element) { ++ final Comparator comparator = this.comparator; ++ for (Link curr = this.head; curr != null; curr = curr.next) { ++ if (comparator.compare(element, curr.element) == 0) { ++ this.removeNode(curr); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public boolean removeLast(final E element) { ++ final Comparator comparator = this.comparator; ++ for (Link curr = this.tail; curr != null; curr = curr.prev) { ++ if (comparator.compare(element, curr.element) == 0) { ++ this.removeNode(curr); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return new Iterator<>() { ++ private Link next = LinkedSortedSet.this.head; ++ ++ @Override ++ public boolean hasNext() { ++ return this.next != null; ++ } ++ ++ @Override ++ public E next() { ++ final Link next = this.next; ++ if (next == null) { ++ throw new NoSuchElementException(); ++ } ++ this.next = next.next; ++ return next.element; ++ } ++ }; ++ } ++ ++ public E pollFirst() { ++ final Link head = this.head; ++ if (head == null) { ++ return null; ++ } ++ ++ final E ret = head.element; ++ final Link next = head.next; ++ ++ // unlink head ++ this.head = next; ++ if (next == null) { ++ this.tail = null; ++ } else { ++ next.prev = null; ++ } ++ ++ // help GC ++ head.element = null; ++ head.next = null; ++ ++ return ret; ++ } ++ ++ public E pollLast() { ++ final Link tail = this.tail; ++ if (tail == null) { ++ return null; ++ } ++ ++ final E ret = tail.element; ++ final Link prev = tail.prev; ++ ++ // unlink tail ++ this.tail = prev; ++ if (prev == null) { ++ this.head = null; ++ } else { ++ prev.next = null; ++ } ++ ++ // help GC ++ tail.element = null; ++ tail.prev = null; ++ ++ return ret; ++ } ++ ++ public Link addLast(final E element) { ++ final Comparator comparator = this.comparator; ++ ++ Link curr = this.tail; ++ if (curr != null) { ++ int compare; ++ ++ while ((compare = comparator.compare(element, curr.element)) < 0) { ++ Link prev = curr; ++ curr = curr.prev; ++ if (curr != null) { ++ continue; ++ } ++ return this.head = prev.prev = new Link<>(element, null, prev); ++ } ++ ++ if (compare != 0) { ++ // insert after curr ++ final Link next = curr.next; ++ final Link insert = new Link<>(element, curr, next); ++ curr.next = insert; ++ ++ if (next == null) { ++ this.tail = insert; ++ } else { ++ next.prev = insert; ++ } ++ return insert; ++ } ++ ++ return null; ++ } else { ++ return this.head = this.tail = new Link<>(element); ++ } ++ } ++ ++ public Link addFirst(final E element) { ++ final Comparator comparator = this.comparator; ++ ++ Link curr = this.head; ++ if (curr != null) { ++ int compare; ++ ++ while ((compare = comparator.compare(element, curr.element)) > 0) { ++ Link prev = curr; ++ curr = curr.next; ++ if (curr != null) { ++ continue; ++ } ++ return this.tail = prev.next = new Link<>(element, prev, null); ++ } ++ ++ if (compare != 0) { ++ // insert before curr ++ final Link prev = curr.prev; ++ final Link insert = new Link<>(element, prev, curr); ++ curr.prev = insert; ++ ++ if (prev == null) { ++ this.head = insert; ++ } else { ++ prev.next = insert; ++ } ++ return insert; ++ } ++ ++ return null; ++ } else { ++ return this.head = this.tail = new Link<>(element); ++ } ++ } ++ ++ public static final class Link { ++ private E element; ++ private Link prev; ++ private Link next; ++ ++ private Link() {} ++ ++ private Link(final E element) { ++ this.element = element; ++ } ++ ++ private Link(final E element, final Link prev, final Link next) { ++ this.element = element; ++ this.prev = prev; ++ this.next = next; ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index ae5dd08de75a7ed231295f306fd0974da3988249..79aa36ec0e8b78787db2428f96913cb7f80c79d3 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -66,7 +66,7 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy + + public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) { + this(output, pos, rot, world, level, name, displayName, server, entity, false, (commandcontext, flag, j) -> { +- }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server)); ++ }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate((Runnable run) -> { io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(run);})); // Paper - region threading + } + + protected CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity, boolean silent, @Nullable ResultConsumer consumer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext signedArguments, TaskChainer messageChainTaskQueue) { +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index 330f6c79417378da855326b4da665f9d240e748d..47f3c31db4bb80cbecc3c26ec56caad91c41f3a6 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -139,12 +139,12 @@ public class Commands { + AdvancementCommands.register(this.dispatcher); + AttributeCommand.register(this.dispatcher, commandRegistryAccess); + ExecuteCommand.register(this.dispatcher, commandRegistryAccess); +- BossBarCommands.register(this.dispatcher); ++ //BossBarCommands.register(this.dispatcher); // Paper - region threading - TODO + ClearInventoryCommands.register(this.dispatcher, commandRegistryAccess); +- CloneCommands.register(this.dispatcher, commandRegistryAccess); +- DataCommands.register(this.dispatcher); +- DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Paper - region threading - TODO ++ //DataCommands.register(this.dispatcher); // Paper - region threading - TODO ++ //DataPackCommand.register(this.dispatcher); // Paper - region threading - TODO ++ //DebugCommand.register(this.dispatcher); // Paper - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, commandRegistryAccess); +@@ -154,44 +154,44 @@ public class Commands { + FillCommand.register(this.dispatcher, commandRegistryAccess); + FillBiomeCommand.register(this.dispatcher, commandRegistryAccess); + ForceLoadCommand.register(this.dispatcher); +- FunctionCommand.register(this.dispatcher); ++ //FunctionCommand.register(this.dispatcher); // Paper - region threading - TODO + GameModeCommand.register(this.dispatcher); + GameRuleCommand.register(this.dispatcher); + GiveCommand.register(this.dispatcher, commandRegistryAccess); + HelpCommand.register(this.dispatcher); +- ItemCommands.register(this.dispatcher, commandRegistryAccess); ++ //ItemCommands.register(this.dispatcher, commandRegistryAccess); // Paper - region threading - TODO later + KickCommand.register(this.dispatcher); + KillCommand.register(this.dispatcher); + ListPlayersCommand.register(this.dispatcher); + LocateCommand.register(this.dispatcher, commandRegistryAccess); +- LootCommand.register(this.dispatcher, commandRegistryAccess); ++ //LootCommand.register(this.dispatcher, commandRegistryAccess); // Paper - region threading - TODO later + MsgCommand.register(this.dispatcher); + ParticleCommand.register(this.dispatcher, commandRegistryAccess); + PlaceCommand.register(this.dispatcher); + PlaySoundCommand.register(this.dispatcher); +- ReloadCommand.register(this.dispatcher); ++ //ReloadCommand.register(this.dispatcher); // Paper - region threading + RecipeCommand.register(this.dispatcher); + SayCommand.register(this.dispatcher); +- ScheduleCommand.register(this.dispatcher); +- ScoreboardCommand.register(this.dispatcher); ++ //ScheduleCommand.register(this.dispatcher); // Paper - region threading ++ //ScoreboardCommand.register(this.dispatcher); // Paper - region threading - TODO later + SeedCommand.register(this.dispatcher, environment != Commands.CommandSelection.INTEGRATED); + SetBlockCommand.register(this.dispatcher, commandRegistryAccess); + SetSpawnCommand.register(this.dispatcher); + SetWorldSpawnCommand.register(this.dispatcher); +- SpectateCommand.register(this.dispatcher); +- SpreadPlayersCommand.register(this.dispatcher); ++ //SpectateCommand.register(this.dispatcher); // Paper - region threading - TODO later ++ //SpreadPlayersCommand.register(this.dispatcher); // Paper - region threading - TODO later + StopSoundCommand.register(this.dispatcher); + SummonCommand.register(this.dispatcher, commandRegistryAccess); +- TagCommand.register(this.dispatcher); +- TeamCommand.register(this.dispatcher); +- TeamMsgCommand.register(this.dispatcher); ++ //TagCommand.register(this.dispatcher); // Paper - region threading - TODO later ++ //TeamCommand.register(this.dispatcher); // Paper - region threading - TODO later ++ //TeamMsgCommand.register(this.dispatcher); // Paper - region threading - TODO later + TeleportCommand.register(this.dispatcher); + TellRawCommand.register(this.dispatcher); + TimeCommand.register(this.dispatcher); + TitleCommand.register(this.dispatcher); +- TriggerCommand.register(this.dispatcher); ++ //TriggerCommand.register(this.dispatcher); // Paper - region threading - TODO later + WeatherCommand.register(this.dispatcher); +- WorldBorderCommand.register(this.dispatcher); ++ //WorldBorderCommand.register(this.dispatcher); // Paper - region threading - TODO later + if (JvmProfiler.INSTANCE.isAvailable()) { + JfrCommand.register(this.dispatcher); + } +@@ -208,8 +208,8 @@ public class Commands { + OpCommand.register(this.dispatcher); + PardonCommand.register(this.dispatcher); + PardonIpCommand.register(this.dispatcher); +- PerfCommand.register(this.dispatcher); +- SaveAllCommand.register(this.dispatcher); ++ //PerfCommand.register(this.dispatcher); // Paper - region threading - TODO later ++ //SaveAllCommand.register(this.dispatcher); // Paper - region threading - TODO later + SaveOffCommand.register(this.dispatcher); + SaveOnCommand.register(this.dispatcher); + SetPlayerIdleTimeoutCommand.register(this.dispatcher); +@@ -417,9 +417,12 @@ public class Commands { + } + // Paper start - Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper +- net.minecraft.server.MinecraftServer.getServer().execute(() -> { +- runSync(player, bukkit, rootcommandnode); +- }); ++ // Paper start - region threading ++ // ignore if retired ++ player.getBukkitEntity().taskScheduler.schedule((updatedPlayer) -> { ++ runSync((ServerPlayer)updatedPlayer, bukkit, rootcommandnode); ++ }, null, 1L); ++ // Paper end - region threading + } + + private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { +diff --git a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java +index 309ad5a1da6b3a297d5526cd9247359ac5f49406..6690e72c2f2ef39f1b127f5459b3991d16c69087 100644 +--- a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java +@@ -33,7 +33,7 @@ public abstract class AbstractProjectileDispenseBehavior extends DefaultDispense + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) ((float) enumdirection.getStepY() + 0.1F), (double) enumdirection.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +index 958134519befadc27a5b647caf64acf272ee2db4..81a4c04a512d152ba96b347bbc612af9924cb373 100644 +--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +@@ -58,7 +58,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +index 1e6ba6d9cceda1d4867b183c3dbc03d317ed287f..4d11d80dc3df7ff439d11155f8eb23feadea8b68 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +@@ -74,7 +74,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 58fa7b99dc7a9745afe6faf31c1804e95ed27dbe..694cdd790fb128c1648797dba314debc1493df54 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -221,7 +221,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -276,7 +276,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -329,7 +329,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -385,7 +385,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorseabstract.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -459,7 +459,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -498,7 +498,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(enumdirection.getStepX(), enumdirection.getStepY(), enumdirection.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -556,7 +556,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d3, d4, d5)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -628,7 +628,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -701,7 +701,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -748,7 +748,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -809,7 +809,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -827,7 +827,8 @@ public interface DispenseItemBehavior { + } + } + +- worldserver.captureTreeGeneration = true; ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = worldserver.getCurrentWorldData(); // Paper - region threading ++ worldData.captureTreeGeneration = true; // Paper - region threading + // CraftBukkit end + + if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) { +@@ -836,13 +837,13 @@ public interface DispenseItemBehavior { + worldserver.levelEvent(1505, blockposition, 0); + } + // CraftBukkit start +- worldserver.captureTreeGeneration = false; +- if (worldserver.capturedBlockStates.size() > 0) { ++ worldData.captureTreeGeneration = false; // Paper - region threading ++ if (worldData.capturedBlockStates.size() > 0) { // Paper - region threading + TreeType treeType = SaplingBlock.treeType; + SaplingBlock.treeType = null; + Location location = new Location(worldserver.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ()); +- List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values()); +- worldserver.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + StructureGrowEvent structureEvent = null; + if (treeType != null) { + structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks); +@@ -877,7 +878,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -934,7 +935,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -983,7 +984,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -1056,7 +1057,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index d1127d93a85a837933d0d73c24cacac4adc3a5b9..536e0b75f70ba93e73c9cad92252aa86e248be77 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -40,7 +40,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +index 0159ed9cbc644c39fa79e62327f13375193fdc98..ce86619e2d8a394df24d2ea7113b0f38e5859e62 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + pointer.getLevel().getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 38c09c65dfa4a7a0c80d36f726c1fd028cbe05f8..e522c13a733aaf6228e981a9baecca8c44ec2e71 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -73,7 +73,7 @@ public class Connection extends SimpleChannelInboundHandler> { + return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper + }); + private final PacketFlow receiving; +- private final Queue queue = Queues.newConcurrentLinkedQueue(); ++ private final Queue queue = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); + public Channel channel; + public SocketAddress address; + // Spigot Start +@@ -81,7 +81,7 @@ public class Connection extends SimpleChannelInboundHandler> { + public com.mojang.authlib.properties.Property[] spoofedProfile; + public boolean preparing = true; + // Spigot End +- private PacketListener packetListener; ++ private volatile PacketListener packetListener; // Paper - region threading + private Component disconnectedReason; + private boolean encrypted; + private boolean disconnectionHandled; +@@ -177,6 +177,32 @@ public class Connection extends SimpleChannelInboundHandler> { + this.receiving = side; + } + ++ // Paper start - region threading ++ private volatile boolean becomeActive; ++ ++ public boolean becomeActive() { ++ return this.becomeActive; ++ } ++ ++ private static record DisconnectReq(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} ++ ++ private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = ++ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); ++ ++ /** ++ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the ++ * same thread that could disconnect. ++ */ ++ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { ++ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); ++ // We can't halt packet processing here because a plugin could cancel a kick request. ++ } ++ ++ public final boolean isPlayerConnected() { ++ return this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl; ++ } ++ // Paper end - region threading ++ + public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception { + super.channelActive(channelhandlercontext); + this.channel = channelhandlercontext.channel(); +@@ -191,6 +217,7 @@ public class Connection extends SimpleChannelInboundHandler> { + Connection.LOGGER.error(LogUtils.FATAL_MARKER, "Failed to change protocol to handshake", throwable); + } + ++ this.becomeActive = true; // Paper - region threading + } + + public void setProtocol(ConnectionProtocol state) { +@@ -372,13 +399,6 @@ public class Connection extends SimpleChannelInboundHandler> { + return; // Do nothing + } + packet.onPacketDispatch(getPlayer()); +- if (connected && (InnerUtil.canSendImmediate(this, packet) || ( +- io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() && +- (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty()) +- ))) { +- this.sendPacket(packet, callbacks, null); // Paper +- return; +- } + // write the packets to the queue, then flush - antixray hooks there already + java.util.List extraPackets = InnerUtil.buildExtraPackets(packet); + boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty(); +@@ -496,66 +516,58 @@ public class Connection extends SimpleChannelInboundHandler> { + + // Paper start - rewrite this to be safer if ran off main thread + private boolean flushQueue() { // void -> boolean +- if (!isConnected()) { ++ if (!this.isConnected()) { + return true; + } +- if (io.papermc.paper.util.MCUtil.isMainThread()) { +- return processQueue(); +- } else if (isPending) { +- // Should only happen during login/status stages +- synchronized (this.queue) { +- return this.processQueue(); +- } +- } +- return false; ++ return this.processQueue(); ++ } ++ ++ // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent ++ // into the queue ++ private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); ++ ++ private boolean canWritePackets() { ++ PacketHolder holder = this.queue.peek(); ++ return holder != null && holder.packet.isReady(); + } ++ + private boolean processQueue() { +- try { // Paper - add pending task queue +- if (this.queue.isEmpty()) return true; +- // Paper start - make only one flush call per sendPacketQueue() call + final boolean needsFlush = this.canFlush; +- boolean hasWrotePacket = false; +- // Paper end - make only one flush call per sendPacketQueue() call +- // If we are on main, we are safe here in that nothing else should be processing queue off main anymore +- // But if we are not on main due to login/status, the parent is synchronized on packetQueue +- java.util.Iterator iterator = this.queue.iterator(); +- while (iterator.hasNext()) { +- PacketHolder queued = iterator.next(); // poll -> peek +- +- // Fix NPE (Spigot bug caused by handleDisconnection()) +- if (false && queued == null) { // Paper - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here +- return true; +- } ++ while (this.canWritePackets()) { ++ final boolean set = this.flushingQueue.getAndSet(true); ++ try { ++ if (set) { ++ // we didn't acquire the lock, break ++ return false; ++ } + +- // Paper start - checking isConsumed flag and skipping packet sending +- if (queued.isConsumed()) { +- continue; +- } +- // Paper end - checking isConsumed flag and skipping packet sending ++ boolean justFlushed = true; + +- Packet packet = queued.packet; +- if (!packet.isReady()) { +- // Paper start - make only one flush call per sendPacketQueue() call +- if (hasWrotePacket && (needsFlush || this.canFlush)) { ++ PacketHolder holder; ++ for (;;) { ++ // synchronise so that queue clears appear atomic ++ synchronized (this.queue) { ++ holder = ((ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.queue).pollIf((PacketHolder h) -> { ++ return h.packet.isReady(); ++ }); ++ } ++ if (holder == null) { ++ break; ++ } ++ justFlushed = (!this.canWritePackets() && (needsFlush || this.canFlush)); ++ this.sendPacket(holder.packet, holder.listener, justFlushed ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call ++ } ++ ++ if (!justFlushed) { + this.flush(); + } +- // Paper end - make only one flush call per sendPacketQueue() call +- return false; +- } else { +- iterator.remove(); +- if (queued.tryMarkConsumed()) { // Paper - try to mark isConsumed flag for de-duplicating packet +- this.sendPacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call +- hasWrotePacket = true; // Paper - make only one flush call per sendPacketQueue() call ++ } finally { ++ if (!set) { ++ this.flushingQueue.set(false); + } + } + } + return true; +- } finally { // Paper start - add pending task queue +- Runnable r; +- while ((r = this.pendingTasks.poll()) != null) { +- this.channel.eventLoop().execute(r); +- } +- } // Paper end - add pending task queue + } + // Paper end + +@@ -564,21 +576,41 @@ public class Connection extends SimpleChannelInboundHandler> { + private static int currTick; // Paper + public void tick() { + this.flushQueue(); +- // Paper start +- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { +- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; +- Connection.joinAttemptsThisTick = 0; ++ // Paper start - region threading ++ // handle disconnect requests, but only after flushQueue() ++ DisconnectReq disconnectReq; ++ while ((disconnectReq = this.disconnectReqs.poll()) != null) { ++ PacketListener packetlistener = this.packetListener; ++ ++ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ loginPacketListener.disconnect(disconnectReq.disconnectReason); ++ // this doesn't fail, so abort any further attempts ++ return; ++ } else if (packetlistener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { ++ gamePacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); ++ // may be cancelled by a plugin, if not cancelled then any further calls do nothing ++ continue; ++ } else { ++ // no idea what packet to send ++ this.disconnect(disconnectReq.disconnectReason); ++ this.setReadOnly(); ++ return; ++ } + } +- // Paper end ++ if (!this.isConnected()) { ++ // disconnected from above ++ this.handleDisconnection(); ++ return; ++ } ++ // Paper end - region threading ++ // Paper - this is broken + PacketListener packetlistener = this.packetListener; + + if (packetlistener instanceof TickablePacketListener) { + TickablePacketListener tickablepacketlistener = (TickablePacketListener) packetlistener; + + // Paper start - limit the number of joins which can be processed each tick +- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) +- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT +- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { ++ if (true) { // Paper - region threading + // Paper start - detailed watchdog information + net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); + try { // Paper end - detailed watchdog information +@@ -618,13 +650,21 @@ public class Connection extends SimpleChannelInboundHandler> { + // Paper start + public void clearPacketQueue() { + net.minecraft.server.level.ServerPlayer player = getPlayer(); +- queue.forEach(queuedPacket -> { ++ java.util.List queuedPackets = new java.util.ArrayList<>(); ++ // synchronise so that flushQueue does not poll values while the queue is being cleared ++ synchronized (this.queue) { ++ Connection.PacketHolder packetHolder; ++ while ((packetHolder = this.queue.poll()) != null) { ++ queuedPackets.add(packetHolder); ++ } ++ } ++ ++ for (Connection.PacketHolder queuedPacket : queuedPackets) { + Packet packet = queuedPacket.packet; + if (packet.hasFinishListener()) { + packet.onPacketDispatchFinish(player, null); + } +- }); +- queue.clear(); ++ } + } + // Paper end + public void disconnect(Component disconnectReason) { +@@ -636,6 +676,7 @@ public class Connection extends SimpleChannelInboundHandler> { + this.channel.close(); // We can't wait as this may be called from an event loop. + this.disconnectedReason = disconnectReason; + } ++ this.becomeActive = true; // Paper - region threading + + } + +@@ -784,13 +825,27 @@ public class Connection extends SimpleChannelInboundHandler> { + final net.minecraft.server.network.ServerGamePacketListenerImpl playerConnection = (net.minecraft.server.network.ServerGamePacketListenerImpl) packetListener; + new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(playerConnection.player.getUUID(), + playerConnection.player.getScoreboardName(), ((java.net.InetSocketAddress)address).getAddress(), false).callEvent(); ++ // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails ++ // Paper start - region threading ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ playerConnection.player.getScoreboardName(), ++ playerConnection.player.getUUID(), this ++ ); ++ // Paper end - region threading + } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl) { + /* Player is login stage */ + final net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener = (net.minecraft.server.network.ServerLoginPacketListenerImpl) packetListener; +- switch (loginListener.state) { +- case READY_TO_ACCEPT: +- case DELAY_ACCEPT: +- case ACCEPTED: ++ // Paper start - region threading ++ if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) { ++ // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ loginListener.gameProfile.getName(), ++ net.minecraft.core.UUIDUtil.getOrCreatePlayerUUID(loginListener.gameProfile), ++ this ++ ); ++ } ++ // Paper end - region threading ++ if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) { // Paper - region threading - rewrite login process + final com.mojang.authlib.GameProfile profile = loginListener.gameProfile; /* Should be non-null at this stage */ + new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(), + ((java.net.InetSocketAddress)address).getAddress(), false).callEvent(); +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index 27d4aa45e585842c04491839826d405d6f447f0e..6cec00f409e8d9b90aa0b238620d2b7dca61f86c 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -2,6 +2,7 @@ package net.minecraft.network.protocol; + + import com.mojang.logging.LogUtils; + import net.minecraft.network.PacketListener; ++import net.minecraft.server.level.ServerPlayer; + import org.slf4j.Logger; + + // CraftBukkit start +@@ -41,7 +42,7 @@ public class PacketUtils { + + public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + if (!engine.isSameThread()) { +- engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown. ++ Runnable run = () -> { // Paper - region threading + packetProcessing.push(listener); // Paper - detailed watchdog information + try { // Paper - detailed watchdog information + if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590 +@@ -71,7 +72,17 @@ public class PacketUtils { + } + // Paper end - detailed watchdog information + +- }); ++ }; // Paper start - region threading ++ ServerGamePacketListenerImpl actualListener = (ServerGamePacketListenerImpl)listener; ++ // ignore retired state, if removed then we don't want the packet to be handled ++ actualListener.player.getBukkitEntity().taskScheduler.schedule( ++ (ServerPlayer player) -> { ++ run.run(); ++ }, ++ null, ++ 1L ++ ); ++ // Paper end - region threading + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; + // CraftBukkit start - SPIGOT-5477, MC-142590 + } else if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) { +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 2ee4e5e8d17a3a1e6a342c74b13135df030ffef6..33b1cdd7c8d655a61868dd464303d137f5fec6ff 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -291,7 +291,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +@@ -304,12 +304,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); + Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system +@@ -602,7 +630,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4); ++ world.randomSpawnSelection = new ChunkPos(world.getChunkSource().randomState().sampler().findSpawnPosition()); ++ for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { ++ for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { ++ ChunkPos pos = new ChunkPos(currX, currZ); ++ world.chunkSource.addTicketAtLevel( ++ TicketType.UNKNOWN, pos, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos ++ ); ++ } ++ } ++ // Paper end - region threading + + // Paper - move up + this.getPlayerList().addWorldborderListener(world); +@@ -614,6 +656,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ return scheduledEnd - System.nanoTime() > targetBuffer; ++ }; ++ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper ++ // Paper end - region threading + co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper +- long i = Util.getNanos(); ++ long i = startTime; // Paper - region threading + + // Paper start - move oversleep into full server tick ++ if (region == null) { // Paper - region threading + isOversleep = true;MinecraftTimings.serverOversleep.startTiming(); + this.managedBlock(() -> { + return !this.canOversleep(); + }); + isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); ++ } // Paper - region threading + // Paper end +- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper ++ // Paper - region threading - move up ++ ++ // Paper start - region threading ++ if (region != null) { ++ region.getTaskQueueData().drainTasks(); ++ // now run all the entity schedulers ++ // TODO there has got to be a more efficient variant of this crap ++ for (Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { ++ continue; ++ } ++ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); ++ if (bukkit != null) { ++ bukkit.taskScheduler.executeTick(); ++ } ++ } ++ // now tick connections ++ region.world.getCurrentWorldData().tickConnections(); // Paper - region threading ++ } ++ // Paper end - region threading + + ++this.tickCount; +- this.tickChildren(shouldKeepTicking); +- if (i - this.lastServerStatus >= 5000000000L) { ++ this.tickChildren(shouldKeepTicking, region); // Paper - region threading ++ if (region == null && i - this.lastServerStatus >= 5000000000L) { // Paper - region threading - moved to global tick + this.lastServerStatus = i; + this.status.setPlayers(new ServerStatus.Players(this.getMaxPlayers(), this.getPlayerCount())); + if (!this.hidesOnlinePlayers()) { +@@ -1429,9 +1571,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { + this.playerList.saveAll(playerSaveInterval); + } +- for (ServerLevel level : this.getAllLevels()) { ++ for (ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Paper - region threading + if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { +- level.saveIncrementally(fullSave); ++ level.saveIncrementally(region == null && fullSave); // Paper - region threading - don't save level.dat + } + } + } finally { +@@ -1441,16 +1583,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper +- worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper +- net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper ++ // Paper - region threading + + this.profiler.push(() -> { + return worldserver + " " + worldserver.dimension().location(); +@@ -1532,7 +1674,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().invalidateStatus(); ++ }); ++ return; ++ } ++ // Paper end - region threading + this.lastServerStatus = 0L; + } + +@@ -1962,6 +2113,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { +- lastMidTickExecuteFailure = currTime; ++ worldData.lastMidTickExecuteFailure = currTime; // Paper - region threading + } + + // note: negative values reduce the time +@@ -2764,7 +2901,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { // Paper - region threading ++ operation.perform(serverPlayer, selection); ++ }, null, 1L); // Paper - region threading ++ + } + + if (i == 0) { +@@ -99,9 +103,13 @@ public class AdvancementCommands { + throw new CommandRuntimeException(Component.translatable("commands.advancement.criterionNotFound", advancement.getChatComponent(), criterion)); + } else { + for(ServerPlayer serverPlayer : targets) { ++ ++i; // Paper - region threading ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading + if (operation.performCriterion(serverPlayer, advancement, criterion)) { +- ++i; ++ // Paper - region threading + } ++ }, null, 1L); // Paper - region threading ++ + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/AttributeCommand.java b/src/main/java/net/minecraft/server/commands/AttributeCommand.java +index e846bd5db018f79c083d29f8f7b305a3d7ab45f5..8adf0bdf19de696b110c59b898e7ba25ae0535c0 100644 +--- a/src/main/java/net/minecraft/server/commands/AttributeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/AttributeCommand.java +@@ -92,58 +92,113 @@ public class AttributeCommand { + } + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int getAttributeValue(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); +- double d = livingEntity.getAttributeValue(attribute); +- source.sendSuccess(Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), target.getName(), d), false); +- return (int)(d * multiplier); ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Paper - region threading ++ double d = livingEntity.getAttributeValue(attribute); ++ source.sendSuccess(Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Paper end - region threading + } + + private static int getAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); +- double d = livingEntity.getAttributeBaseValue(attribute); +- source.sendSuccess(Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), target.getName(), d), false); +- return (int)(d * multiplier); ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); ++ double d = livingEntity.getAttributeBaseValue(attribute); ++ source.sendSuccess(Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Paper end - region threading ++ + } + + private static int getAttributeModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid, double multiplier) throws CommandSyntaxException { +- LivingEntity livingEntity = getEntityWithAttribute(target, attribute); +- AttributeMap attributeMap = livingEntity.getAttributes(); +- if (!attributeMap.hasModifier(attribute, uuid)) { +- throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), uuid); +- } else { +- double d = attributeMap.getModifierValue(attribute, uuid); +- source.sendSuccess(Component.translatable("commands.attribute.modifier.value.get.success", uuid, getAttributeDescription(attribute), target.getName(), d), false); +- return (int)(d * multiplier); +- } ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); ++ AttributeMap attributeMap = livingEntity.getAttributes(); ++ if (!attributeMap.hasModifier(attribute, uuid)) { ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); ++ } else { ++ double d = attributeMap.getModifierValue(attribute, uuid); ++ source.sendSuccess(Component.translatable("commands.attribute.modifier.value.get.success", uuid, getAttributeDescription(attribute), nmsEntity.getName(), d), false); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Paper end - region threading + } + + private static int setAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double value) throws CommandSyntaxException { +- getAttributeInstance(target, attribute).setBaseValue(value); +- source.sendSuccess(Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), target.getName(), value), false); ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ getAttributeInstance(nmsEntity, attribute).setBaseValue(value); ++ source.sendSuccess(Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false); ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ // Paper end - region threading + return 1; + } + + private static int addModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid, String name, double value, AttributeModifier.Operation operation) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(target, attribute); +- AttributeModifier attributeModifier = new AttributeModifier(uuid, name, value, operation); +- if (attributeInstance.hasModifier(attributeModifier)) { +- throw ERROR_MODIFIER_ALREADY_PRESENT.create(target.getName(), getAttributeDescription(attribute), uuid); +- } else { +- attributeInstance.addPermanentModifier(attributeModifier); +- source.sendSuccess(Component.translatable("commands.attribute.modifier.add.success", uuid, getAttributeDescription(attribute), target.getName()), false); +- return 1; +- } ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); ++ AttributeModifier attributeModifier = new AttributeModifier(uuid, name, value, operation); ++ if (attributeInstance.hasModifier(attributeModifier)) { ++ throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); ++ } else { ++ attributeInstance.addPermanentModifier(attributeModifier); ++ source.sendSuccess(Component.translatable("commands.attribute.modifier.add.success", uuid, getAttributeDescription(attribute), nmsEntity.getName()), false); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 1; ++ // Paper end - region threading + } + + private static int removeModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(target, attribute); +- if (attributeInstance.removePermanentModifier(uuid)) { +- source.sendSuccess(Component.translatable("commands.attribute.modifier.remove.success", uuid, getAttributeDescription(attribute), target.getName()), false); +- return 1; +- } else { +- throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), uuid); +- } ++ // Paper start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); ++ if (attributeInstance.removePermanentModifier(uuid)) { ++ source.sendSuccess(Component.translatable("commands.attribute.modifier.remove.success", uuid, getAttributeDescription(attribute), nmsEntity.getName()), false); ++ } else { ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 1; ++ // Paper end - region threading + } + + private static Component getAttributeDescription(Holder attribute) { +diff --git a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +index 74623df731de543d3ef5832e818b10adec7b0f01..f852441fbc4f04d619dad48a51a9438ae683358a 100644 +--- a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java ++++ b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +@@ -46,9 +46,12 @@ public class ClearInventoryCommands { + int i = 0; + + for(ServerPlayer serverPlayer : targets) { +- i += serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading ++ serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); + serverPlayer.containerMenu.broadcastChanges(); + serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); ++ }, null, 1L); // Paper - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +index 1bf4c5b36f53ef1e71d50d1a9af8e1410e5dff60..0f7c2baed057242b0107ec525ac034fe8388ba51 100644 +--- a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java ++++ b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +@@ -25,12 +25,14 @@ public class DefaultGameModeCommands { + GameType gameType = minecraftServer.getForcedGameType(); + if (gameType != null) { + for(ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading + // Paper start - extend PlayerGameModeChangeEvent + org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); + if (event != null && event.isCancelled()) { + source.sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); + } + // Paper end ++ }, null, 1L); // Paper - region threading + ++i; + } + } +diff --git a/src/main/java/net/minecraft/server/commands/EffectCommands.java b/src/main/java/net/minecraft/server/commands/EffectCommands.java +index bed3ffb18398f34077503ba2d7aa6ecc7c0537c2..672ab95a8c2afe4d074ebbcd68a247a827522f49 100644 +--- a/src/main/java/net/minecraft/server/commands/EffectCommands.java ++++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java +@@ -76,7 +76,15 @@ public class EffectCommands { + if (entity instanceof LivingEntity) { + MobEffectInstance mobeffect = new MobEffectInstance(mobeffectlist, k, amplifier, false, showParticles); + +- if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ // Paper start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).addEffect(mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Paper end - region threading ++ if (true) { // CraftBukkit // Paper - region threading + ++j; + } + } +@@ -102,8 +110,16 @@ public class EffectCommands { + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity) { // CraftBukkit // Paper - region threading + ++i; ++ // Paper start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Paper end - region threading + } + } + +@@ -128,8 +144,16 @@ public class EffectCommands { + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity) { // CraftBukkit // Paper - region threading + ++i; ++ // Paper start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ if (!(nmsEntity instanceof LivingEntity)) { ++ return; ++ } ++ ((LivingEntity) nmsEntity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Paper end - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +index e639c0ec642910e66b1d68ae0b9208ef58d91fce..c17b8ed18952c1ab7e9f12ea7d39a4c94bcc3232 100644 +--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java ++++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java +@@ -46,6 +46,12 @@ public class EnchantCommand { + }))))); + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment2 = enchantment.value(); + if (level > enchantment2.getMaxLevel()) { +@@ -55,18 +61,26 @@ public class EnchantCommand { + + for(Entity entity : targets) { + if (entity instanceof LivingEntity) { +- LivingEntity livingEntity = (LivingEntity)entity; +- ItemStack itemStack = livingEntity.getMainHandItem(); +- if (!itemStack.isEmpty()) { +- if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { +- itemStack.enchant(enchantment2, level); +- ++i; +- } else if (targets.size() == 1) { +- throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); ++ // Paper start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = (LivingEntity)nmsEntity; ++ ItemStack itemStack = livingEntity.getMainHandItem(); ++ if (!itemStack.isEmpty()) { ++ if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { ++ itemStack.enchant(enchantment2, level); ++ } else if (targets.size() == 1) { ++ throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); ++ } ++ } else if (targets.size() == 1) { ++ throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); + } +- } else if (targets.size() == 1) { +- throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); +- } ++ }, null, 1L); ++ ++i; ++ // Paper end - region threading + } else if (targets.size() == 1) { + throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); + } +diff --git a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +index a628e3730b1c26c2e6a85c449440af0afe4c0d8d..1be09bf6356e02300dd7854695ec7a677b8ed5f4 100644 +--- a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java ++++ b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +@@ -46,14 +46,18 @@ public class ExperienceCommand { + } + + private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type component) { ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer p) -> { // Paper - region threading + int i = component.query.applyAsInt(player); + source.sendSuccess(Component.translatable("commands.experience.query." + component.name, player.getDisplayName(), i), false); +- return i; ++ }, null, 1L); // Paper - region threading ++ return 0; + } + + private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type component) { + for(ServerPlayer serverPlayer : targets) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading + component.add.accept(serverPlayer, amount); ++ }, null, 1L); // Paper - region threading + } + + if (targets.size() == 1) { +@@ -69,9 +73,12 @@ public class ExperienceCommand { + int i = 0; + + for(ServerPlayer serverPlayer : targets) { ++ ++i; // Paper - region threading ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading + if (component.set.test(serverPlayer, amount)) { +- ++i; ++ // Paper - region threading + } ++ }, null, 1L); // Paper - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +index 6c29947dc9259f453782de3c973c1cabb87e3de5..aed35fdb60bd60a0afd25b41142b58ea1b0a17bb 100644 +--- a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +@@ -69,6 +69,12 @@ public class FillBiomeCommand { + }; + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int fill(CommandSourceStack source, BlockPos from, BlockPos to, Holder.Reference biome, Predicate> filter) throws CommandSyntaxException { + BlockPos blockPos = quantize(from); + BlockPos blockPos2 = quantize(to); +@@ -78,29 +84,43 @@ public class FillBiomeCommand { + throw ERROR_VOLUME_TOO_LARGE.create(32768, i); + } else { + ServerLevel serverLevel = source.getLevel(); +- List list = new ArrayList<>(); ++ // Paper start - region threading ++ int buffer = 0; ++ // no buffer, we do not touch neighbours ++ serverLevel.loadChunksAsync( ++ (boundingBox.minX() - buffer) >> 4, ++ (boundingBox.maxX() + buffer) >> 4, ++ (boundingBox.minZ() - buffer) >> 4, ++ (boundingBox.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (chunks) -> { ++ List list = new ArrayList<>(); + +- for(int j = SectionPos.blockToSectionCoord(boundingBox.minZ()); j <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++j) { +- for(int k = SectionPos.blockToSectionCoord(boundingBox.minX()); k <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++k) { +- ChunkAccess chunkAccess = serverLevel.getChunk(k, j, ChunkStatus.FULL, false); +- if (chunkAccess == null) { +- throw ERROR_NOT_LOADED.create(); +- } ++ for(int j = SectionPos.blockToSectionCoord(boundingBox.minZ()); j <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++j) { ++ for(int k = SectionPos.blockToSectionCoord(boundingBox.minX()); k <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++k) { ++ ChunkAccess chunkAccess = serverLevel.getChunk(k, j, ChunkStatus.FULL, false); ++ if (chunkAccess == null) { ++ sendMessage(source, ERROR_NOT_LOADED.create()); return; ++ } + +- list.add(chunkAccess); +- } +- } ++ list.add(chunkAccess); ++ } ++ } + +- MutableInt mutableInt = new MutableInt(0); ++ MutableInt mutableInt = new MutableInt(0); + +- for(ChunkAccess chunkAccess2 : list) { +- chunkAccess2.fillBiomesFromNoise(makeResolver(mutableInt, chunkAccess2, boundingBox, biome, filter), serverLevel.getChunkSource().randomState().sampler()); +- chunkAccess2.setUnsaved(true); +- serverLevel.getChunkSource().chunkMap.resendChunk(chunkAccess2); +- } ++ for(ChunkAccess chunkAccess2 : list) { ++ chunkAccess2.fillBiomesFromNoise(makeResolver(mutableInt, chunkAccess2, boundingBox, biome, filter), serverLevel.getChunkSource().randomState().sampler()); ++ chunkAccess2.setUnsaved(true); ++ serverLevel.getChunkSource().chunkMap.resendChunk(chunkAccess2); ++ } + +- source.sendSuccess(Component.translatable("commands.fillbiome.success.count", mutableInt.getValue(), boundingBox.minX(), boundingBox.minY(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxY(), boundingBox.maxZ()), true); +- return mutableInt.getValue(); ++ source.sendSuccess(Component.translatable("commands.fillbiome.success.count", mutableInt.getValue(), boundingBox.minX(), boundingBox.minY(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxY(), boundingBox.maxZ()), true); ++ } ++ ); ++ return 0; ++ // Paper end - region threading + } + } + } +diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java +index 99fbb24dabe867ed4956a2996543107f58a57193..c6b748060aa120bea3be9723a46b83ecea3f5b66 100644 +--- a/src/main/java/net/minecraft/server/commands/FillCommand.java ++++ b/src/main/java/net/minecraft/server/commands/FillCommand.java +@@ -57,6 +57,12 @@ public class FillCommand { + })))))); + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int fillBlocks(CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter) throws CommandSyntaxException { + int i = range.getXSpan() * range.getYSpan() * range.getZSpan(); + if (i > 32768) { +@@ -64,33 +70,50 @@ public class FillCommand { + } else { + List list = Lists.newArrayList(); + ServerLevel serverLevel = source.getLevel(); +- int j = 0; + +- for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { +- if (filter == null || filter.test(new BlockInWorld(serverLevel, blockPos, true))) { +- BlockInput blockInput = mode.filter.filter(range, blockPos, block, serverLevel); +- if (blockInput != null) { +- BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos); +- Clearable.tryClear(blockEntity); +- if (blockInput.place(serverLevel, blockPos, 2)) { +- list.add(blockPos.immutable()); +- ++j; ++ // Paper start - region threading ++ int buffer = 32; ++ // physics may spill into neighbour chunks, so use a buffer ++ serverLevel.loadChunksAsync( ++ (range.minX() - buffer) >> 4, ++ (range.maxX() + buffer) >> 4, ++ (range.minZ() - buffer) >> 4, ++ (range.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (chunks) -> { ++ int j = 0; ++ ++ for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { ++ if (filter == null || filter.test(new BlockInWorld(serverLevel, blockPos, true))) { ++ BlockInput blockInput = mode.filter.filter(range, blockPos, block, serverLevel); ++ if (blockInput != null) { ++ BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos); ++ Clearable.tryClear(blockEntity); ++ if (blockInput.place(serverLevel, blockPos, 2)) { ++ list.add(blockPos.immutable()); ++ ++j; ++ } ++ } + } + } +- } +- } + +- for(BlockPos blockPos2 : list) { +- Block block2 = serverLevel.getBlockState(blockPos2).getBlock(); +- serverLevel.blockUpdated(blockPos2, block2); +- } ++ for(BlockPos blockPos2 : list) { ++ Block block2 = serverLevel.getBlockState(blockPos2).getBlock(); ++ serverLevel.blockUpdated(blockPos2, block2); ++ } ++ ++ if (j == 0) { ++ sendMessage(source, ERROR_FAILED.create()); return; // Paper - region threading ++ } else { ++ source.sendSuccess(Component.translatable("commands.fill.success", j), true); ++ return; // Paper - region threading ++ } ++ } ++ ); + +- if (j == 0) { +- throw ERROR_FAILED.create(); +- } else { +- source.sendSuccess(Component.translatable("commands.fill.success", j), true); +- return j; +- } ++ return 0; ++ // Paper end - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +index de484336165891d16220fdc0363e5283ba92b75d..0ab53877345ec69a2babc199a1489768cdaf330e 100644 +--- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java ++++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +@@ -49,96 +49,126 @@ public class ForceLoadCommand { + })))); + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { + ChunkPos chunkPos = pos.toChunkPos(); + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); +- boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong()); +- if (bl) { +- source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false); +- return 1; +- } else { +- throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); +- } ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { ++ try { ++ boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong()); ++ if (bl) { ++ source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false); ++ return; ++ } else { ++ throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Paper end - region threading + } + + private static int listForceLoad(CommandSourceStack source) { + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); +- LongSet longSet = serverLevel.getForcedChunks(); +- int i = longSet.size(); +- if (i > 0) { +- String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator()); +- if (i == 1) { +- source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false); ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { ++ LongSet longSet = serverLevel.getForcedChunks(); ++ int i = longSet.size(); ++ if (i > 0) { ++ String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator()); ++ if (i == 1) { ++ source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false); ++ } else { ++ source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false); ++ } + } else { +- source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false); ++ source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location())); + } +- } else { +- source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location())); +- } + +- return i; ++ }); ++ return 1; ++ // Paper end - region threading + } + + private static int removeAll(CommandSourceStack source) { + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + LongSet longSet = serverLevel.getForcedChunks(); + longSet.forEach((chunkPos) -> { + serverLevel.setChunkForced(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos), false); + }); + source.sendSuccess(Component.translatable("commands.forceload.removed.all", resourceKey.location()), true); ++ }); // Paper - region threading + return 0; + } + + private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean forceLoaded) throws CommandSyntaxException { +- int i = Math.min(from.x(), to.x()); +- int j = Math.min(from.z(), to.z()); +- int k = Math.max(from.x(), to.x()); +- int l = Math.max(from.z(), to.z()); +- if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) { +- int m = SectionPos.blockToSectionCoord(i); +- int n = SectionPos.blockToSectionCoord(j); +- int o = SectionPos.blockToSectionCoord(k); +- int p = SectionPos.blockToSectionCoord(l); +- long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L); +- if (q > 256L) { +- throw ERROR_TOO_MANY_CHUNKS.create(256, q); +- } else { +- ServerLevel serverLevel = source.getLevel(); +- ResourceKey resourceKey = serverLevel.dimension(); +- ChunkPos chunkPos = null; +- int r = 0; ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { ++ try { ++ int i = Math.min(from.x(), to.x()); ++ int j = Math.min(from.z(), to.z()); ++ int k = Math.max(from.x(), to.x()); ++ int l = Math.max(from.z(), to.z()); ++ if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) { ++ int m = SectionPos.blockToSectionCoord(i); ++ int n = SectionPos.blockToSectionCoord(j); ++ int o = SectionPos.blockToSectionCoord(k); ++ int p = SectionPos.blockToSectionCoord(l); ++ long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L); ++ if (q > 256L) { ++ throw ERROR_TOO_MANY_CHUNKS.create(256, q); ++ } else { ++ ServerLevel serverLevel = source.getLevel(); ++ ResourceKey resourceKey = serverLevel.dimension(); ++ ChunkPos chunkPos = null; ++ int r = 0; + +- for(int s = m; s <= o; ++s) { +- for(int t = n; t <= p; ++t) { +- boolean bl = serverLevel.setChunkForced(s, t, forceLoaded); +- if (bl) { +- ++r; +- if (chunkPos == null) { +- chunkPos = new ChunkPos(s, t); ++ for(int s = m; s <= o; ++s) { ++ for(int t = n; t <= p; ++t) { ++ boolean bl = serverLevel.setChunkForced(s, t, forceLoaded); ++ if (bl) { ++ ++r; ++ if (chunkPos == null) { ++ chunkPos = new ChunkPos(s, t); ++ } ++ } + } + } +- } +- } + +- if (r == 0) { +- throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create(); +- } else { +- if (r == 1) { +- source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true); +- } else { +- ChunkPos chunkPos2 = new ChunkPos(m, n); +- ChunkPos chunkPos3 = new ChunkPos(o, p); +- source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true); +- } ++ if (r == 0) { ++ throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create(); ++ } else { ++ if (r == 1) { ++ source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true); ++ } else { ++ ChunkPos chunkPos2 = new ChunkPos(m, n); ++ ChunkPos chunkPos3 = new ChunkPos(o, p); ++ source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true); ++ } + +- return r; ++ return; ++ } ++ } ++ } else { ++ throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); + } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); + } +- } else { +- throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); +- } ++ }); ++ return 1; ++ // Paper end - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +index 27c0aaf123c3e945eb24e8a3892bd8ac42115733..a8fa32601ccc07f67f3ac58867529a5cded088e8 100644 +--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java +@@ -44,15 +44,18 @@ public class GameModeCommand { + int i = 0; + + for(ServerPlayer serverPlayer : targets) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading + // Paper start - extend PlayerGameModeChangeEvent + org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); + if (event != null && !event.isCancelled()) { + logGamemodeChange(context.getSource(), serverPlayer, gameMode); +- ++i; ++ // Paper - region threading + } else if (event != null && event.cancelMessage() != null) { + context.getSource().sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); + // Paper end + } ++ }, null, 1L); // Paper - region threading ++ ++i; // Paper - region threading + } + + return i; +diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java +index 06e3a868e922f1b7a586d0ca28f64a67ae463b68..49d6b02129fe4c8347bd12793608efab8d8101eb 100644 +--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java ++++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java +@@ -55,6 +55,7 @@ public class GiveCommand { + + l -= i1; + ItemStack itemstack = item.createItemStack(i1, false); ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading + boolean flag = entityplayer.getInventory().add(itemstack); + ItemEntity entityitem; + +@@ -74,6 +75,7 @@ public class GiveCommand { + entityitem.setOwner(entityplayer.getUUID()); + } + } ++ }, null, 1L); // Paper - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/commands/KillCommand.java b/src/main/java/net/minecraft/server/commands/KillCommand.java +index a6e4bd9243dab7feaed1bd968108a324d6c37ed7..793d2735a65d5f09ed081961265ab7fa3c067448 100644 +--- a/src/main/java/net/minecraft/server/commands/KillCommand.java ++++ b/src/main/java/net/minecraft/server/commands/KillCommand.java +@@ -22,7 +22,9 @@ public class KillCommand { + + private static int kill(CommandSourceStack source, Collection targets) { + for(Entity entity : targets) { +- entity.kill(); ++ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading ++ nmsEntity.kill(); // Paper - region threading ++ }, null, 1L); // Paper - region threading + } + + if (targets.size() == 1) { +diff --git a/src/main/java/net/minecraft/server/commands/PlaceCommand.java b/src/main/java/net/minecraft/server/commands/PlaceCommand.java +index 6835072c6b30ee0b79c43e05526fd6d605bf7139..59b8f1796873a76b703f5e97ae5bf972ad0942ec 100644 +--- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java ++++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java +@@ -83,82 +83,130 @@ public class PlaceCommand { + }))))))))); + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); + ConfiguredFeature configuredFeature = feature.value(); + ChunkPos chunkPos = new ChunkPos(pos); + checkLoaded(serverLevel, new ChunkPos(chunkPos.x - 1, chunkPos.z - 1), new ChunkPos(chunkPos.x + 1, chunkPos.z + 1)); +- if (!configuredFeature.place(serverLevel, serverLevel.getChunkSource().getGenerator(), serverLevel.getRandom(), pos)) { +- throw ERROR_FEATURE_FAILED.create(); +- } else { +- String string = feature.key().location().toString(); +- source.sendSuccess(Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; +- } ++ // Paper start - region threading ++ serverLevel.loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ if (!configuredFeature.place(serverLevel, serverLevel.getChunkSource().getGenerator(), serverLevel.getRandom(), pos)) { ++ throw ERROR_FEATURE_FAILED.create(); ++ } else { ++ String string = feature.key().location().toString(); ++ source.sendSuccess(Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Paper end - region threading + } + + public static int placeJigsaw(CommandSourceStack source, Holder structurePool, ResourceLocation id, int maxDepth, BlockPos pos) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); +- if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) { +- throw ERROR_JIGSAW_FAILED.create(); +- } else { +- source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; +- } ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) { ++ throw ERROR_JIGSAW_FAILED.create(); ++ } else { ++ source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Paper end - region threading + } + + public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); + Structure structure2 = structure.value(); + ChunkGenerator chunkGenerator = serverLevel.getChunkSource().getGenerator(); +- StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> { +- return true; +- }); +- if (!structureStart.isValid()) { +- throw ERROR_STRUCTURE_FAILED.create(); +- } else { +- BoundingBox boundingBox = structureStart.getBoundingBox(); +- ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); +- ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); +- checkLoaded(serverLevel, chunkPos, chunkPos2); +- ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> { +- structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx); +- }); +- String string = structure.key().location().toString(); +- source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; +- } ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> { ++ return true; ++ }); ++ if (!structureStart.isValid()) { ++ throw ERROR_STRUCTURE_FAILED.create(); ++ } else { ++ BoundingBox boundingBox = structureStart.getBoundingBox(); ++ ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); ++ ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); ++ checkLoaded(serverLevel, chunkPos, chunkPos2); ++ ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> { ++ structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx); ++ }); ++ String string = structure.key().location().toString(); ++ source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Paper end - region threading + } + + public static int placeTemplate(CommandSourceStack source, ResourceLocation id, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); +- StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager(); ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager(); + +- Optional optional; +- try { +- optional = structureTemplateManager.get(id); +- } catch (ResourceLocationException var13) { +- throw ERROR_TEMPLATE_INVALID.create(id); +- } ++ Optional optional; ++ try { ++ optional = structureTemplateManager.get(id); ++ } catch (ResourceLocationException var13) { ++ throw ERROR_TEMPLATE_INVALID.create(id); ++ } + +- if (optional.isEmpty()) { +- throw ERROR_TEMPLATE_INVALID.create(id); +- } else { +- StructureTemplate structureTemplate = optional.get(); +- checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize()))); +- StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation); +- if (integrity < 1.0F) { +- structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed)); +- } ++ if (optional.isEmpty()) { ++ throw ERROR_TEMPLATE_INVALID.create(id); ++ } else { ++ StructureTemplate structureTemplate = optional.get(); ++ checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize()))); ++ StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation); ++ if (integrity < 1.0F) { ++ structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed)); ++ } + +- boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2); +- if (!bl) { +- throw ERROR_TEMPLATE_FAILED.create(); +- } else { +- source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2); ++ if (!bl) { ++ throw ERROR_TEMPLATE_FAILED.create(); ++ } else { ++ source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true); ++ } ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } + } +- } ++ ); ++ return 1; ++ // Paper end - region threading + } + + private static void checkLoaded(ServerLevel world, ChunkPos pos1, ChunkPos pos2) throws CommandSyntaxException { +diff --git a/src/main/java/net/minecraft/server/commands/RecipeCommand.java b/src/main/java/net/minecraft/server/commands/RecipeCommand.java +index 2a92e542e4b3e4dfb26adfc4b21490a629b79382..980bf5e6af4f8835f9841d2dc92a58ce236f205e 100644 +--- a/src/main/java/net/minecraft/server/commands/RecipeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/RecipeCommand.java +@@ -36,7 +36,12 @@ public class RecipeCommand { + int i = 0; + + for(ServerPlayer serverPlayer : targets) { +- i += serverPlayer.awardRecipes(recipes); ++ // Paper start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ serverPlayer.awardRecipes(recipes); ++ }, null, 1L); ++ // Paper end - region threading + } + + if (i == 0) { +@@ -56,7 +61,12 @@ public class RecipeCommand { + int i = 0; + + for(ServerPlayer serverPlayer : targets) { +- i += serverPlayer.resetRecipes(recipes); ++ // Paper start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ serverPlayer.resetRecipes(recipes); ++ }, null, 1L); ++ // Paper end - region threading + } + + if (i == 0) { +diff --git a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +index ad435815e56ca5a8d5ea6046ee4a3ed4d3673a48..175296e956b118d8095792ed68b95739b64582a6 100644 +--- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +@@ -38,29 +38,45 @@ public class SetBlockCommand { + }))))); + } + ++ // Paper start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Paper end - region threading ++ + private static int setBlock(CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate condition) throws CommandSyntaxException { + ServerLevel serverLevel = source.getLevel(); +- if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { +- throw ERROR_FAILED.create(); +- } else { +- boolean bl; +- if (mode == SetBlockCommand.Mode.DESTROY) { +- serverLevel.destroyBlock(pos, true); +- bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir(); +- } else { +- BlockEntity blockEntity = serverLevel.getBlockEntity(pos); +- Clearable.tryClear(blockEntity); +- bl = true; +- } ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { ++ throw ERROR_FAILED.create(); ++ } else { ++ boolean bl; ++ if (mode == SetBlockCommand.Mode.DESTROY) { ++ serverLevel.destroyBlock(pos, true); ++ bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir(); ++ } else { ++ BlockEntity blockEntity = serverLevel.getBlockEntity(pos); ++ Clearable.tryClear(blockEntity); ++ bl = true; ++ } + +- if (bl && !block.place(serverLevel, pos, 2)) { +- throw ERROR_FAILED.create(); +- } else { +- serverLevel.blockUpdated(pos, block.getState().getBlock()); +- source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ if (bl && !block.place(serverLevel, pos, 2)) { ++ throw ERROR_FAILED.create(); ++ } else { ++ serverLevel.blockUpdated(pos, block.getState().getBlock()); ++ source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); ++ } ++ } ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } + } +- } ++ ); ++ return 1; ++ // Paper end - region threading + } + + public interface Filter { +diff --git a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +index 1e41de9523c5fa3b9cfced798a5c35a24ec9d349..a4fbc369e20831642bc9c9141e6a8bd7a95c39cd 100644 +--- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +@@ -35,7 +35,12 @@ public class SetSpawnCommand { + final Collection actualTargets = new java.util.ArrayList<>(); // Paper + for(ServerPlayer serverPlayer : targets) { + // Paper start - PlayerSetSpawnEvent +- if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { ++ // Paper start - region threading ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Paper end - region threading ++ if (true) { // Paper - region threading + actualTargets.add(serverPlayer); + } + // Paper end +diff --git a/src/main/java/net/minecraft/server/commands/SummonCommand.java b/src/main/java/net/minecraft/server/commands/SummonCommand.java +index ade2626bc63f986a53277378cdc19f5366f9372f..d0f181dadaf7a116c50e0278b8b8fc5c8433165d 100644 +--- a/src/main/java/net/minecraft/server/commands/SummonCommand.java ++++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java +@@ -63,11 +63,18 @@ public class SummonCommand { + if (entity == null) { + throw SummonCommand.ERROR_FAILED.create(); + } else { +- if (initialize && entity instanceof Mob) { +- ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null); +- } ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> { ++ if (initialize && entity instanceof Mob) { ++ ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null); ++ } ++ worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); ++ } ++ ); ++ // Paper end - region threading + +- if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" ++ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Paper - region threading + throw SummonCommand.ERROR_DUPLICATE_UUID.create(); + } else { + source.sendSuccess(Component.translatable("commands.summon.success", entity.getDisplayName()), true); +diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +index 027ca5b67c544048815ddef4bb36d0a8fc3d038c..34e97bb14e5b215cff2632f6917798a87ff1d7c5 100644 +--- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java ++++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +@@ -78,7 +78,7 @@ public class TeleportCommand { + while (iterator.hasNext()) { + Entity entity1 = (Entity) iterator.next(); + +- TeleportCommand.performTeleport(source, entity1, (ServerLevel) destination.level, destination.getX(), destination.getY(), destination.getZ(), EnumSet.noneOf(ClientboundPlayerPositionPacket.RelativeArgument.class), destination.getYRot(), destination.getXRot(), (TeleportCommand.LookAt) null); ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity1, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Paper - region threading + } + + if (targets.size() == 1) { +@@ -154,6 +154,24 @@ public class TeleportCommand { + float f2 = Mth.wrapDegrees(yaw); + float f3 = Mth.wrapDegrees(pitch); + ++ // Paper start - region threading ++ if (true) { ++ ServerLevel worldFinal = world; ++ Vec3 posFinal = new Vec3(x, y, z); ++ Float yawFinal = Float.valueOf(f2); ++ Float pitchFinal = Float.valueOf(f3); ++ target.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { ++ nmsEntity.unRide(); ++ nmsEntity.teleportAsync( ++ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ }, null, 1L); ++ return; ++ } ++ // Paper end - region threading + if (target instanceof ServerPlayer) { + ChunkPos chunkcoordintpair = new ChunkPos(new BlockPos(x, y, z)); + +diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java +index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..866c6c843ed06bc8c88fff0210f1e8d820d4c928 100644 +--- a/src/main/java/net/minecraft/server/commands/TimeCommand.java ++++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java +@@ -56,6 +56,7 @@ public class TimeCommand { + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + // CraftBukkit start + TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime()); + Bukkit.getPluginManager().callEvent(event); +@@ -63,6 +64,7 @@ public class TimeCommand { + worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount()); + } + // CraftBukkit end ++ }); // Paper - region threading + } + + source.sendSuccess(Component.translatable("commands.time.set", time), true); +@@ -75,6 +77,7 @@ public class TimeCommand { + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + // CraftBukkit start + TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time); + Bukkit.getPluginManager().callEvent(event); +@@ -82,11 +85,14 @@ public class TimeCommand { + worldserver.setDayTime(worldserver.getDayTime() + event.getSkipAmount()); + } + // CraftBukkit end ++ }); // Paper - region threading + } + ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + int j = TimeCommand.getDayTime(source.getLevel()); + + source.sendSuccess(Component.translatable("commands.time.set", j), true); +- return j; ++ }); // Paper - region threading ++ return 0; // Paper - region threading + } + } +diff --git a/src/main/java/net/minecraft/server/commands/WeatherCommand.java b/src/main/java/net/minecraft/server/commands/WeatherCommand.java +index 71fd7887a4fa174d3f74c4bbe24497b156cbd3c8..e10b98e171ab83c82c09cc68f67afd57a6228c03 100644 +--- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java ++++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java +@@ -28,20 +28,26 @@ public class WeatherCommand { + } + + private static int setClear(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + source.getLevel().setWeatherParameters(duration, 0, false, false); + source.sendSuccess(Component.translatable("commands.weather.set.clear"), true); ++ }); // Paper - region threading + return duration; + } + + private static int setRain(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + source.getLevel().setWeatherParameters(0, duration, true, false); + source.sendSuccess(Component.translatable("commands.weather.set.rain"), true); ++ }); // Paper - region threading + return duration; + } + + private static int setThunder(CommandSourceStack source, int duration) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + source.getLevel().setWeatherParameters(0, duration, true, true); + source.sendSuccess(Component.translatable("commands.weather.set.thunder"), true); ++ }); // Paper - region threading + return duration; + } + } +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 51b3db0b6c2cede95b584268e035c0fb36d38094..417cf9050091543f79cc463480e56735118790f7 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -436,9 +436,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + + @Override +- public void tickChildren(BooleanSupplier shouldKeepTicking) { +- super.tickChildren(shouldKeepTicking); +- this.handleConsoleInputs(); ++ public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Paper - region threading ++ super.tickChildren(shouldKeepTicking, region); // Paper - region threading ++ if (region == null) this.handleConsoleInputs(); // Paper - region threading + } + + @Override +@@ -741,6 +741,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + + @Override + public String runCommand(String command) { ++ // Paper start - region threading ++ // RIP RCON ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ // Paper end - region threading + Waitable[] waitableArray = new Waitable[1]; + this.rconConsoleSource.prepareForCommand(); + this.executeBlocking(() -> { +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 0b9cb85c063f913ad9245bafb8587d2f06c0ac6e..e7a93ebf2e91fe649c4a1704294a65cc04f8a315 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -85,18 +85,18 @@ public class ChunkHolder { + public void onChunkAdd() { + // Paper start - optimise anyPlayerCloseEnoughForSpawning + long key = io.papermc.paper.util.MCUtil.getCoordinateKey(this.pos); +- this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); +- this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); ++ this.playersInMobSpawnRange = null; // Paper - region threading ++ this.playersInChunkTickRange = null; // Paper - region threading + // Paper end - optimise anyPlayerCloseEnoughForSpawning + // Paper start - optimise chunk tick iteration + if (this.needsBroadcastChanges()) { +- this.chunkMap.needsChangeBroadcasting.add(this); ++ this.chunkMap.level.getCurrentWorldData().addChunkHolderNeedsBroadcasting(this); // Paper - region threading + } + // Paper end - optimise chunk tick iteration + // Paper start - optimise checkDespawn + LevelChunk chunk = this.getFullChunkNowUnchecked(); + if (chunk != null) { +- chunk.updateGeneralAreaCache(); ++ //chunk.updateGeneralAreaCache(); // Paper - region threading + } + // Paper end - optimise checkDespawn + } +@@ -108,13 +108,13 @@ public class ChunkHolder { + // Paper end - optimise anyPlayerCloseEnoughForSpawning + // Paper start - optimise chunk tick iteration + if (this.needsBroadcastChanges()) { +- this.chunkMap.needsChangeBroadcasting.remove(this); ++ this.chunkMap.level.getCurrentWorldData().removeChunkHolderNeedsBroadcasting(this); // Paper - region threading + } + // Paper end - optimise chunk tick iteration + // Paper start - optimise checkDespawn + LevelChunk chunk = this.getFullChunkNowUnchecked(); + if (chunk != null) { +- chunk.removeGeneralAreaCache(); ++ //chunk.removeGeneralAreaCache(); // Paper - region threading + } + // Paper end - optimise checkDespawn + } +@@ -303,7 +303,7 @@ public class ChunkHolder { + + private void addToBroadcastMap() { + org.spigotmc.AsyncCatcher.catchOp("ChunkHolder update"); +- this.chunkMap.needsChangeBroadcasting.add(this); ++ this.chunkMap.level.getCurrentWorldData().addChunkHolderNeedsBroadcasting(this); // Paper - region threading + } + // Paper end - optimise chunk tick iteration + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 870f4d6fae8c14502b4653f246a2df9e345ccca3..3d6ea2ccf27afd1b57cac4776b630defdffd1c65 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -146,21 +146,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final AtomicInteger tickingGenerated; + public final StructureTemplateManager structureTemplateManager; // Paper - rewrite chunk system + private final String storageName; +- private final PlayerMap playerMap; +- public final Int2ObjectMap entityMap; ++ //private final PlayerMap playerMap; // Paper - region threading ++ //public final Int2ObjectMap entityMap; // Paper - region threading + private final Long2ByteMap chunkTypeCache; + private final Long2LongMap chunkSaveCooldowns; + private final Queue unloadQueue; + int viewDistance; +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper +- public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); ++ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper // Paper - region threading ++ //public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); // Paper - region threading + + // Paper - rewrite chunk system + // Paper start - optimise checkDespawn + public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; ++ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; // Paper - region threading + // Paper end - optimise checkDespawn + + // Paper start - distance maps +@@ -174,8 +174,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // obviously this means a spawn range > 8 cannot be implemented + + // these maps are named after spigot's uses +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; ++ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick // Paper - region threading ++ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; // Paper - region threading + // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + // Paper start - use distance map to optimise tracker + public static boolean isLegacyTrackingEntity(Entity entity) { +@@ -184,11 +184,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // inlined EnumMap, TrackingRange.TrackingRangeType + static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); +- public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; +- final int[] entityTrackerTrackRanges; +- public final int getEntityTrackerRange(final int ordinal) { +- return this.entityTrackerTrackRanges[ordinal]; +- } ++ // Paper - region threading + + private int convertSpigotRangeToVanilla(final int vanilla) { + return MinecraftServer.getServer().getScaledTrackingDistance(vanilla); +@@ -200,40 +196,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated +- this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning ++ //this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper - region threading + // Paper start - per player mob spawning +- if (this.playerMobDistanceMap != null) { +- this.playerMobDistanceMap.add(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player)); +- } ++ // Paper - region threading + // Paper end - per player mob spawning + // Paper start - use distance map to optimise entity tracker +- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { +- com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; +- int trackRange = this.entityTrackerTrackRanges[i]; +- +- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); +- } ++ // Paper - region threading + // Paper end - use distance map to optimise entity tracker +- this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn ++ //this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Paper - region threading + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { + this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader + + // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning +- this.playerMobSpawnMap.remove(player); +- this.playerChunkTickRangeMap.remove(player); ++ this.level.getCurrentWorldData().mobSpawnMap.remove(player); // Paper - region threading ++ //this.playerChunkTickRangeMap.remove(player); // Paper - region threading + // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning +- this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns ++ //this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns // Paper - region threading + // Paper start - per player mob spawning +- if (this.playerMobDistanceMap != null) { +- this.playerMobDistanceMap.remove(player); +- } ++ // Paper - region threading + // Paper end - per player mob spawning + // Paper start - use distance map to optimise tracker +- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { +- this.playerEntityTrackerTrackMaps[i].remove(player); +- } ++ // Paper - region threading + // Paper end - use distance map to optimise tracker + } + +@@ -242,21 +227,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader +- this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning ++ //this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper - region threading + // Paper start - per player mob spawning +- if (this.playerMobDistanceMap != null) { +- this.playerMobDistanceMap.update(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player)); +- } ++ // Paper - region threading + // Paper end - per player mob spawning + // Paper start - use distance map to optimise entity tracker +- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { +- com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; +- int trackRange = this.entityTrackerTrackRanges[i]; +- +- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); +- } ++ // Paper - region threading + // Paper end - use distance map to optimise entity tracker +- this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn ++ //this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Paper - region threading + } + // Paper end + // Paper start +@@ -294,8 +272,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); + // Paper - rewrite chunk system + this.tickingGenerated = new AtomicInteger(); +- this.playerMap = new PlayerMap(); +- this.entityMap = new Int2ObjectOpenHashMap(); ++ //this.playerMap = new PlayerMap(); // Paper - region threading ++ //this.entityMap = new Int2ObjectOpenHashMap(); // Paper - region threading + this.chunkTypeCache = new Long2ByteOpenHashMap(); + this.chunkSaveCooldowns = new Long2LongOpenHashMap(); + this.unloadQueue = Queues.newConcurrentLinkedQueue(); +@@ -340,96 +318,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.setViewDistance(viewDistance); + // Paper start + this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); +- this.regionManagers.add(this.dataRegionManager); ++ //this.regionManagers.add(this.dataRegionManager); // Paper - region threading + // Paper end +- this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper ++ //this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper // Paper - region threading + // Paper start - use distance map to optimise entity tracker +- this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length]; +- this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length]; +- +- org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig; +- +- for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) { +- org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal]; +- int configuredSpigotValue; +- switch (trackingRangeType) { +- case PLAYER: +- configuredSpigotValue = spigotWorldConfig.playerTrackingRange; +- break; +- case ANIMAL: +- configuredSpigotValue = spigotWorldConfig.animalTrackingRange; +- break; +- case MONSTER: +- configuredSpigotValue = spigotWorldConfig.monsterTrackingRange; +- break; +- case MISC: +- configuredSpigotValue = spigotWorldConfig.miscTrackingRange; +- break; +- case OTHER: +- configuredSpigotValue = spigotWorldConfig.otherTrackingRange; +- break; +- case ENDERDRAGON: +- configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16; +- break; +- default: +- throw new IllegalStateException("Missing case for enum " + trackingRangeType); +- } +- configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue); +- +- int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0); +- this.entityTrackerTrackRanges[ordinal] = trackRange; +- +- this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); +- } ++ // Paper - region threading + // Paper end - use distance map to optimise entity tracker + // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning +- this.playerChunkTickRangeMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); +- if (playerChunk != null) { +- playerChunk.playersInChunkTickRange = newState; +- } +- }, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); +- if (playerChunk != null) { +- playerChunk.playersInChunkTickRange = newState; +- } +- }); +- this.playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); +- if (playerChunk != null) { +- playerChunk.playersInMobSpawnRange = newState; +- } +- }, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); +- if (playerChunk != null) { +- playerChunk.playersInMobSpawnRange = newState; +- } +- }); ++ // Paper - region threading ++ // Paper - region threading + // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + // Paper start - optimise checkDespawn +- this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); +- if (chunk != null) { +- chunk.updateGeneralAreaCache(newState); +- } +- }, +- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { +- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); +- if (chunk != null) { +- chunk.updateGeneralAreaCache(newState); +- } +- }); ++ // Paper - region threading + // Paper end - optimise checkDespawn + } + +@@ -461,19 +361,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { + return; + } +- int index = entity.getType().getCategory().ordinal(); +- +- final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerMobDistanceMap.getObjectsInRange(entity.chunkPosition()); +- if (inRange == null) { +- return; +- } +- final Object[] backingSet = inRange.getBackingSet(); +- for (int i = 0; i < backingSet.length; i++) { +- if (!(backingSet[i] instanceof final ServerPlayer player)) { +- continue; +- } +- ++player.mobCounts[index]; +- } ++ // Paper - region threading + } + + public int getMobCountNear(ServerPlayer entityPlayer, net.minecraft.world.entity.MobCategory mobCategory) { +@@ -747,6 +635,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper start + // rets true if to prevent the entity from being added + public static boolean checkDupeUUID(ServerLevel level, Entity entity) { ++ // Paper start - region threading ++ if (true) { ++ // TODO fix this shit later ++ return false; ++ } ++ // Paper end - region threading + io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode; + if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN + && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE +@@ -1007,6 +901,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) { ++ // Paper start - region threading ++ if (true) { ++ java.util.List players = this.level.getLocalPlayers(); ++ if (reducedRange) { ++ for (int i = 0, len = players.size(); i < len; ++i) { ++ ServerPlayer player = players.get(i); ++ if (!player.affectsSpawning || player.isSpectator()) { ++ continue; ++ } ++ // don't check spectator and whatnot, already handled by mob spawn map update ++ if (euclideanDistanceSquared(chunkcoordintpair, player) < player.lastEntitySpawnRadiusSquared) { ++ return true; // in range ++ } ++ } ++ } else { ++ final double range = (DistanceManager.MOB_SPAWN_RANGE * 16) * (DistanceManager.MOB_SPAWN_RANGE * 16); ++ // before spigot, mob spawn range was actually mob spawn range + tick range, but it was split ++ for (int i = 0, len = players.size(); i < len; ++i) { ++ ServerPlayer player = players.get(i); ++ if (!player.affectsSpawning || player.isSpectator()) { ++ continue; ++ } ++ // don't check spectator and whatnot, already handled by mob spawn map update ++ if (euclideanDistanceSquared(chunkcoordintpair, player) < range) { ++ return true; // in range ++ } ++ } ++ } ++ // no players in range ++ return false; ++ } ++ // Paper end - region threading + // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance + // tested and confirmed via System.nanoTime() + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange; +@@ -1052,7 +978,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + return List.of(); + } else { + Builder builder = ImmutableList.builder(); +- Iterator iterator = this.playerMap.getPlayers(i).iterator(); ++ Iterator iterator = this.level.getLocalPlayers().iterator(); // Paper - region threading + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +@@ -1081,25 +1007,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + void updatePlayerStatus(ServerPlayer player, boolean added) { +- boolean flag1 = this.skipPlayer(player); +- boolean flag2 = this.playerMap.ignoredOrUnknown(player); +- int i = SectionPos.blockToSectionCoord(player.getBlockX()); +- int j = SectionPos.blockToSectionCoord(player.getBlockZ()); ++ // Paper - region threading + + if (added) { +- this.playerMap.addPlayer(ChunkPos.asLong(i, j), player, flag1); ++ // Paper - region threading + this.updatePlayerPos(player); +- if (!flag1) { +- this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); +- } ++ // Paper - region threading + this.addPlayerToDistanceMaps(player); // Paper - distance maps + } else { + SectionPos sectionposition = player.getLastSectionPos(); + +- this.playerMap.removePlayer(sectionposition.chunk().toLong(), player); +- if (!flag2) { +- this.distanceManager.removePlayer(sectionposition, player); +- } ++ // Paper - region threading ++ // Paper - region threading + this.removePlayerFromDistanceMaps(player); // Paper - distance maps + } + +@@ -1118,43 +1037,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public void move(ServerPlayer player) { + // Paper - delay this logic for the entity tracker tick, no need to duplicate it + +- int i = SectionPos.blockToSectionCoord(player.getBlockX()); +- int j = SectionPos.blockToSectionCoord(player.getBlockZ()); +- SectionPos sectionposition = player.getLastSectionPos(); +- SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); +- long k = sectionposition.chunk().toLong(); +- long l = sectionposition1.chunk().toLong(); +- boolean flag = this.playerMap.ignored(player); +- boolean flag1 = this.skipPlayer(player); +- boolean flag2 = sectionposition.asLong() != sectionposition1.asLong(); +- +- if (flag2 || flag != flag1) { +- this.updatePlayerPos(player); +- if (!flag) { +- this.distanceManager.removePlayer(sectionposition, player); +- } +- +- if (!flag1) { +- this.distanceManager.addPlayer(sectionposition1, player); +- } +- +- if (!flag && flag1) { +- this.playerMap.ignorePlayer(player); +- } +- +- if (flag && !flag1) { +- this.playerMap.unIgnorePlayer(player); +- } +- +- if (k != l) { +- this.playerMap.updatePlayer(k, l, player); +- } +- } +- +- int i1 = sectionposition.x(); +- int j1 = sectionposition.z(); +- int k1; +- int l1; ++ // Paper - region threading - none of this logic is relevant anymore thanks to the player chunk loader + + // Paper - replaced by PlayerChunkLoader + +@@ -1177,9 +1060,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public void addEntity(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot + // Paper start - ignore and warn about illegal addEntity calls instead of crashing server +- if (!entity.valid || entity.level != this.level || this.entityMap.containsKey(entity.getId())) { ++ if (!entity.valid || entity.level != this.level || entity.tracker != null) { // Paper - region threading + LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() +- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); ++ + ": " + entity + (entity.tracker != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); + return; + } + if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Delay adding to tracker until after list packets +@@ -1192,27 +1075,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (i != 0) { + int j = entitytypes.updateInterval(); + +- if (this.entityMap.containsKey(entity.getId())) { ++ if (entity.tracker != null) { // Paper - region threading + throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); + } else { + ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas()); + + entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker +- this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); +- playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players ++ // Paper - region threading ++ playerchunkmap_entitytracker.updatePlayers(this.level.getLocalPlayers()); // Paper - don't search all players // Paper - region threading + if (entity instanceof ServerPlayer) { + ServerPlayer entityplayer = (ServerPlayer) entity; + + this.updatePlayerStatus(entityplayer, true); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- if (playerchunkmap_entitytracker1.entity != entityplayer) { +- playerchunkmap_entitytracker1.updatePlayer(entityplayer); ++ // Paper start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { ++ if (possible.tracker != null) { ++ possible.tracker.updatePlayer(entityplayer); + } + } ++ // Paper end - region threading + } + + } +@@ -1226,16 +1107,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + ServerPlayer entityplayer = (ServerPlayer) entity; + + this.updatePlayerStatus(entityplayer, false); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- playerchunkmap_entitytracker.removePlayer(entityplayer); ++ // Paper start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { ++ if (possible.tracker != null) { ++ possible.tracker.removePlayer(entityplayer); ++ } + } ++ // Paper end - region threading + } + +- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = entity.tracker; // Paper - region threading + + if (playerchunkmap_entitytracker1 != null) { + playerchunkmap_entitytracker1.broadcastRemoved(); +@@ -1245,25 +1126,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - optimised tracker + private final void processTrackQueue() { +- this.level.timings.tracker1.startTiming(); +- try { +- for (TrackedEntity tracker : this.entityMap.values()) { +- // update tracker entry +- tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); +- } +- } finally { +- this.level.timings.tracker1.stopTiming(); +- } +- +- +- this.level.timings.tracker2.startTiming(); +- try { +- for (TrackedEntity tracker : this.entityMap.values()) { +- tracker.serverEntity.sendChanges(); ++ // Paper start - region threading ++ List players = this.level.getLocalPlayers(); // Paper - region threading ++ for (Entity entity : this.level.getCurrentWorldData().getLocalEntities()) { ++ TrackedEntity tracker = entity.tracker; ++ if (tracker == null) { ++ continue; + } +- } finally { +- this.level.timings.tracker2.stopTiming(); ++ tracker.updatePlayers(players); ++ tracker.removeNonTickThreadPlayers(); ++ tracker.serverEntity.sendChanges(); + } ++ // Paper end - region threading + } + // Paper end - optimised tracker + +@@ -1274,51 +1148,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + return; + } + // Paper end - optimized tracker +- List list = Lists.newArrayList(); +- List list1 = this.level.players(); +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- level.timings.tracker1.startTiming(); // Paper +- +- ChunkMap.TrackedEntity playerchunkmap_entitytracker; +- +- while (objectiterator.hasNext()) { +- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos; +- SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity); +- boolean flag = !Objects.equals(sectionposition, sectionposition1); +- +- if (flag) { +- playerchunkmap_entitytracker.updatePlayers(list1); +- Entity entity = playerchunkmap_entitytracker.entity; +- +- if (entity instanceof ServerPlayer) { +- list.add((ServerPlayer) entity); +- } +- +- playerchunkmap_entitytracker.lastSectionPos = sectionposition1; +- } +- +- if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) { +- playerchunkmap_entitytracker.serverEntity.sendChanges(); +- } +- } +- level.timings.tracker1.stopTiming(); // Paper +- +- if (!list.isEmpty()) { +- objectiterator = this.entityMap.values().iterator(); +- +- level.timings.tracker2.startTiming(); // Paper +- while (objectiterator.hasNext()) { +- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- playerchunkmap_entitytracker.updatePlayers(list); +- } +- level.timings.tracker2.stopTiming(); // Paper +- } ++ // Paper - region threading + + } + + public void broadcast(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Paper - region threading + + if (playerchunkmap_entitytracker != null) { + playerchunkmap_entitytracker.broadcast(packet); +@@ -1327,7 +1162,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected void broadcastAndSend(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Paper - region threading + + if (playerchunkmap_entitytracker != null) { + playerchunkmap_entitytracker.broadcastAndSend(packet); +@@ -1504,41 +1339,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.lastSectionPos = SectionPos.of((EntityAccess) entity); + } + +- // Paper start - use distance map to optimise tracker +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; +- +- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; +- this.lastTrackerCandidates = newTrackerCandidates; +- +- if (newTrackerCandidates != null) { +- Object[] rawData = newTrackerCandidates.getBackingSet(); +- for (int i = 0, len = rawData.length; i < len; ++i) { +- Object raw = rawData[i]; +- if (!(raw instanceof ServerPlayer)) { +- continue; +- } +- ServerPlayer player = (ServerPlayer)raw; +- this.updatePlayer(player); +- } +- } +- +- if (oldTrackerCandidates == newTrackerCandidates) { +- // this is likely the case. +- // means there has been no range changes, so we can just use the above for tracking. +- return; +- } +- +- // stuff could have been removed, so we need to check the trackedPlayers set +- // for players that were removed +- +- for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME +- if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { +- this.updatePlayer(conn.getPlayer()); +- } +- } +- } +- // Paper end - use distance map to optimise tracker ++ // Paper - region threading + + public boolean equals(Object object) { + return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false; +@@ -1585,6 +1386,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + } ++ // Paper start - region threading ++ public void removeNonTickThreadPlayers() { ++ boolean foundToRemove = false; ++ for (ServerPlayerConnection conn : this.seenBy) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(conn.getPlayer())) { ++ foundToRemove = true; ++ break; ++ } ++ } ++ ++ if (!foundToRemove) { ++ return; ++ } ++ ++ for (ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { ++ ServerPlayer player = conn.getPlayer(); ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) { ++ this.removePlayer(player); ++ } ++ } ++ } ++ // Paper end - region threading + + public void updatePlayer(ServerPlayer player) { + org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot +@@ -1600,10 +1423,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); + + // CraftBukkit start - respect vanish API +- if (!player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - region threading + flag = false; + } + // CraftBukkit end ++ // Paper start - region threading ++ if ((this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { ++ flag = false; ++ } ++ // Paper end - region threading + if (flag) { + if (this.seenBy.add(player.connection)) { + this.serverEntity.addPairing(player); +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 88fca8b160df6804f30ed2cf8cf1f645085434e2..34f76afe573f312b855fc38e90d978b566acf8af 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -200,14 +200,14 @@ public abstract class DistanceManager { + public int getNaturalSpawnChunkCount() { + // Paper start - use distance map to implement + // note: this is the spawn chunk count +- return this.chunkMap.playerChunkTickRangeMap.size(); ++ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.size(); // Paper - region threading + // Paper end - use distance map to implement + } + + public boolean hasPlayersNearby(long chunkPos) { + // Paper start - use distance map to implement + // note: this is the is spawn chunk method +- return this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(chunkPos) != null; ++ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - region threading + // Paper end - use distance map to implement + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 736f37979c882e41e7571202df38eb6a2923fcb0..1cc6c4d0e9b3b3de2c07de11e8c0b9ec4033f9f8 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -61,7 +61,7 @@ public class ServerChunkCache extends ChunkSource { + public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; + public final ChunkMap chunkMap; + private final DimensionDataStorage dataStorage; +- private long lastInhabitedUpdate; ++ //private long lastInhabitedUpdate; // Paper - region threading + public boolean spawnEnemies = true; + public boolean spawnFriendlies = true; + private static final int CACHE_SIZE = 4; +@@ -72,62 +72,33 @@ public class ServerChunkCache extends ChunkSource { + @VisibleForDebug + private NaturalSpawner.SpawnState lastSpawnState; + // Paper start +- final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); +- final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); ++ // Paper - region threading ++ final ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable loadedChunkMap = new ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable<>(8192, 0.5f); // Paper - region threading + +- private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; ++ // Paper - region threading + + private static int getChunkCacheKey(int x, int z) { + return x & 3 | ((z & 3) << 2); + } + + public void addLoadedChunk(LevelChunk chunk) { +- this.loadedChunkMapSeqLock.acquireWrite(); +- try { ++ synchronized (this.loadedChunkMap) { // Paper - region threading + this.loadedChunkMap.put(chunk.coordinateKey, chunk); +- } finally { +- this.loadedChunkMapSeqLock.releaseWrite(); +- } +- +- // rewrite cache if we have to +- // we do this since we also cache null chunks +- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); ++ } // Paper - region threading + +- this.lastLoadedChunks[cacheKey] = chunk; ++ // Paper - region threading + } + + public void removeLoadedChunk(LevelChunk chunk) { +- this.loadedChunkMapSeqLock.acquireWrite(); +- try { ++ synchronized (this.loadedChunkMap) { // Paper - region threading + this.loadedChunkMap.remove(chunk.coordinateKey); +- } finally { +- this.loadedChunkMapSeqLock.releaseWrite(); +- } ++ } // Paper - region threading + +- // rewrite cache if we have to +- // we do this since we also cache null chunks +- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); +- +- LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; +- if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) { +- this.lastLoadedChunks[cacheKey] = null; +- } ++ // Paper - region threading + } + + public final LevelChunk getChunkAtIfLoadedMainThread(int x, int z) { +- int cacheKey = getChunkCacheKey(x, z); +- +- LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; +- if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) { +- return cachedChunk; +- } +- +- long chunkKey = ChunkPos.asLong(x, z); +- +- cachedChunk = this.loadedChunkMap.get(chunkKey); +- // Skipping a null check to avoid extra instructions to improve inline capability +- this.lastLoadedChunks[cacheKey] = cachedChunk; +- return cachedChunk; ++ return this.loadedChunkMap.get(ChunkPos.asLong(x, z)); // Paper - region threading + } + + public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) { +@@ -142,7 +113,7 @@ public class ServerChunkCache extends ChunkSource { + return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true); + } + +- long chunkFutureAwaitCounter; // Paper - private -> package private ++ final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - private -> package private // Paper - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH + + public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( +@@ -293,8 +264,7 @@ public class ServerChunkCache extends ChunkSource { + this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier); + } + +- public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); +- public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); ++ // Paper - region threading + // Paper end + + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { +@@ -368,26 +338,7 @@ public class ServerChunkCache extends ChunkSource { + public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { + long k = ChunkPos.asLong(x, z); + +- if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system +- return this.getChunkAtIfLoadedMainThread(x, z); +- } +- +- LevelChunk ret = null; +- long readlock; +- do { +- readlock = this.loadedChunkMapSeqLock.acquireRead(); +- try { +- ret = this.loadedChunkMap.get(k); +- } catch (Throwable thr) { +- if (thr instanceof ThreadDeath) { +- throw (ThreadDeath)thr; +- } +- // re-try, this means a CME occurred... +- continue; +- } +- } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock)); +- +- return ret; ++ return this.loadedChunkMap.get(k); // Paper - region threading + } + // Paper end + // Paper start - async chunk io +@@ -483,6 +434,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + CompletableFuture completablefuture; + +@@ -659,10 +611,11 @@ public class ServerChunkCache extends ChunkSource { + } + + private void tickChunks() { ++ io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = this.level.getCurrentWorldData(); // Paper - region threading + long i = this.level.getGameTime(); +- long j = i - this.lastInhabitedUpdate; ++ long j = 1; // Paper - region threading + +- this.lastInhabitedUpdate = i; ++ //this.lastInhabitedUpdate = i; // Paper - region threading + boolean flag = this.level.isDebug(); + + if (flag) { +@@ -670,9 +623,11 @@ public class ServerChunkCache extends ChunkSource { + } else { + // Paper start - optimize isOutisdeRange + ChunkMap playerChunkMap = this.chunkMap; +- for (ServerPlayer player : this.level.players) { ++ // Paper - region threading ++ ++ for (ServerPlayer player : this.level.getLocalPlayers()) { // Paper - region threading + if (!player.affectsSpawning || player.isSpectator()) { +- playerChunkMap.playerMobSpawnMap.remove(player); ++ regionisedWorldData.mobSpawnMap.remove(player); // Paper - region threading + continue; + } + +@@ -685,8 +640,9 @@ public class ServerChunkCache extends ChunkSource { + + com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange); + event.callEvent(); +- if (event.isCancelled() || event.getSpawnRadius() < 0 || playerChunkMap.playerChunkTickRangeMap.getLastViewDistance(player) == -1) { +- playerChunkMap.playerMobSpawnMap.remove(player); ++ if (event.isCancelled() || event.getSpawnRadius() < 0) { // Paper - region threading ++ player.lastEntitySpawnRadiusSquared = -1.0; player.playerNaturallySpawnedEvent = null; // Paper - region threading ++ regionisedWorldData.mobSpawnMap.remove(player); // Paper - region threading + continue; + } + +@@ -694,7 +650,7 @@ public class ServerChunkCache extends ChunkSource { + int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); + +- playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); ++ regionisedWorldData.mobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); // Paper - region threading + player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning + player.playerNaturallySpawnedEvent = event; + } +@@ -704,21 +660,21 @@ public class ServerChunkCache extends ChunkSource { + + gameprofilerfiller.push("pollingChunks"); + int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); +- boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit ++ boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Paper - region threading + + gameprofilerfiller.push("naturalSpawnCount"); + this.level.timings.countNaturalMobs.startTiming(); // Paper - timings + int l = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - per player mob spawning + NaturalSpawner.SpawnState spawnercreature_d; // moved down +- if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled ++ if ((this.spawnFriendlies || this.spawnEnemies)) { // don't count mobs when animals and monsters are disabled // Paper - region threading + // re-set mob counts + for (ServerPlayer player : this.level.players) { + Arrays.fill(player.mobCounts, 0); + } +- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true); ++ spawnercreature_d = NaturalSpawner.createState(l, regionisedWorldData.getLocalEntities(), this::getFullChunk, null, true); // Paper - region threading + } else { +- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ spawnercreature_d = NaturalSpawner.createState(l, regionisedWorldData.getLocalEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap), false); // Paper - region threading + } + // Paper end + this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings +@@ -731,17 +687,17 @@ public class ServerChunkCache extends ChunkSource { + // Paper - moved down + + gameprofilerfiller.popPush("spawnAndTick"); +- boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit ++ boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !regionisedWorldData.getLocalPlayers().isEmpty(); // CraftBukkit // Paper - region threading + + // Paper - only shuffle if per-player mob spawning is disabled + // Paper - moved natural spawn event up + // Paper start - optimise chunk tick iteration + Iterator iterator1; + if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { +- iterator1 = this.entityTickingChunks.iterator(); ++ iterator1 = regionisedWorldData.getEntityTickingChunks().iterator(); // Paper - region threading + } else { +- iterator1 = this.entityTickingChunks.unsafeIterator(); +- List shuffled = Lists.newArrayListWithCapacity(this.entityTickingChunks.size()); ++ iterator1 = regionisedWorldData.getEntityTickingChunks().unsafeIterator(); // Paper - region threading ++ List shuffled = Lists.newArrayListWithCapacity(regionisedWorldData.getEntityTickingChunks().size()); // Paper - region threading + while (iterator1.hasNext()) { + shuffled.add(iterator1.next()); + } +@@ -791,14 +747,14 @@ public class ServerChunkCache extends ChunkSource { + // Paper start - use set of chunks requiring updates, rather than iterating every single one loaded + gameprofilerfiller.popPush("broadcast"); + this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing +- if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { +- ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); +- this.chunkMap.needsChangeBroadcasting.clear(); ++ if (!regionisedWorldData.getNeedsChangeBroadcasting().isEmpty()) { // Paper - region threading ++ ReferenceOpenHashSet copy = regionisedWorldData.getNeedsChangeBroadcasting().clone(); // Paper - region threading ++ regionisedWorldData.getNeedsChangeBroadcasting().clear(); // Paper - region threading + for (ChunkHolder holder : copy) { + holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded + if (holder.needsBroadcastChanges()) { + // I DON'T want to KNOW what DUMB plugins might be doing. +- this.chunkMap.needsChangeBroadcasting.add(holder); ++ regionisedWorldData.getNeedsChangeBroadcasting().add(holder); // Paper - region threading + } + } + } +@@ -806,8 +762,8 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.pop(); + // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded + // Paper start - controlled flush for entity tracker packets +- List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); +- for (ServerPlayer player : this.level.players) { ++ List disabledFlushes = new java.util.ArrayList<>(regionisedWorldData.getLocalPlayers().size()); // Paper - region threading ++ for (ServerPlayer player : regionisedWorldData.getLocalPlayers()) { // Paper - region threading + net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; + if (connection != null) { + connection.connection.disableAutomaticFlush(); +@@ -880,14 +836,19 @@ public class ServerChunkCache extends ChunkSource { + + @Override + public void onLightUpdate(LightLayer type, SectionPos pos) { +- this.mainThreadProcessor.execute(() -> { ++ Runnable run = () -> { // Paper - region threading + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.chunk().toLong()); + + if (playerchunk != null) { + playerchunk.sectionLightChanged(type, pos.y()); + } + +- }); ++ }; // Paper - region threading ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueChunkTask( ++ this.level, pos.getX(), pos.getZ(), run ++ ); ++ // Paper end - region threading + } + + public void addRegionTicket(TicketType ticketType, ChunkPos pos, int radius, T argument) { +@@ -992,8 +953,43 @@ public class ServerChunkCache extends ChunkSource { + return ServerChunkCache.this.mainThread; + } + ++ // Paper start - region threading ++ @Override ++ public void tell(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.tell(runnable); ++ } ++ ++ @Override ++ public void executeBlocking(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeBlocking(runnable); ++ } ++ ++ @Override ++ public void execute(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.execute(runnable); ++ } ++ ++ @Override ++ public void executeIfPossible(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeIfPossible(runnable); ++ } ++ // Paper end - region threading ++ + @Override + protected void doRunTask(Runnable task) { ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); + super.doRunTask(task); + } +@@ -1001,11 +997,16 @@ public class ServerChunkCache extends ChunkSource { + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { ++ // Paper start - region threading ++ if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData().world) { ++ throw new IllegalStateException("Polling tasks from non-owned region"); ++ } ++ // Paper end - region threading + ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader + if (ServerChunkCache.this.runDistanceManagerUpdates()) { + return true; + } +- return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system ++ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Paper - rewrite chunk system // Paper - region threading + } + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 714637cdd9dcdbffa344b19e77944fb3c7541ff7..89752d05e3172a83869695e2c1537924dd7657be 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -192,35 +192,34 @@ public class ServerLevel extends Level implements WorldGenLevel { + public final ServerChunkCache chunkSource; + private final MinecraftServer server; + public final PrimaryLevelData serverLevelData; // CraftBukkit - type +- final EntityTickList entityTickList; ++ //final EntityTickList entityTickList; // Paper - region threading + //public final PersistentEntitySectionManager entityManager; // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; + private int emptyTime; + private final PortalForcer portalForcer; +- private final LevelTicks blockTicks; +- private final LevelTicks fluidTicks; ++ //private final LevelTicks blockTicks; // Paper - region threading ++ //private final LevelTicks fluidTicks; // Paper - region threading + final Set navigatingMobs; + volatile boolean isUpdatingNavigations; + protected final Raids raids; +- private final ObjectLinkedOpenHashSet blockEvents; +- private final List blockEventsToReschedule; +- private boolean handlingTick; ++ //private final ObjectLinkedOpenHashSet blockEvents; // Paper - region threading ++ //private final List blockEventsToReschedule; // Paper - region threading ++ //private boolean handlingTick; // Paper - region threading + private final List customSpawners; + @Nullable + private final EndDragonFight dragonFight; + final Int2ObjectMap dragonParts; + private final StructureManager structureManager; + private final StructureCheck structureCheck; +- private final boolean tickTime; +- public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick ++ public final boolean tickTime; // Paper - region threading ++ // Paper - region threading + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess convertable; + public final UUID uuid; +- public boolean hasPhysicsEvent = true; // Paper +- public boolean hasEntityMoveEvent = false; // Paper ++ // Paper - region threading + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) + public static Throwable getAddToWorldStackTrace(Entity entity) { + final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date()); +@@ -267,50 +266,64 @@ public class ServerLevel extends Level implements WorldGenLevel { + return true; + } + +- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, +- java.util.function.Consumer> onLoad) { +- if (Thread.currentThread() != this.thread) { +- this.getChunkSource().mainThreadProcessor.execute(() -> { +- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); +- }); +- return; +- } ++ // Paper start - region threading - TODO rebase ++ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ priority, onLoad ++ ); ++ } ++ ++ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, ++ net.minecraft.world.level.chunk.ChunkStatus chunkStatus, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ chunkStatus, priority, onLoad ++ ); ++ } ++ ++ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.ChunkStatus.FULL, priority, onLoad); ++ } ++ ++ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, ++ net.minecraft.world.level.chunk.ChunkStatus chunkStatus, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { + List ret = new java.util.ArrayList<>(); +- IntArrayList ticketLevels = new IntArrayList(); +- +- int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; +- int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; +- +- int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; +- int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; +- +- int minChunkX = minBlockX >> 4; +- int maxChunkX = maxBlockX >> 4; +- +- int minChunkZ = minBlockZ >> 4; +- int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); +- int[] loadedChunks = new int[1]; ++ java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); ++ ++ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter.getAndIncrement()); + +- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); ++ int ticketLevel = 33 + net.minecraft.world.level.chunk.ChunkStatus.getDistance(chunkStatus); + + java.util.function.Consumer consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { + if (chunk != null) { +- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); + ret.add(chunk); +- ticketLevels.add(ticketLevel); + chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + } +- if (++loadedChunks[0] == requiredChunks) { ++ if (loadedChunks.incrementAndGet() == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + ChunkPos chunkPos = ret.get(i).getPos(); +- int ticketLevel = ticketLevels.getInt(i); + + chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); +@@ -322,11 +335,31 @@ public class ServerLevel extends Level implements WorldGenLevel { + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( +- this, cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, priority, consumer ++ this, cx, cz, chunkStatus, true, priority, consumer + ); + } + } + } ++ // Paper end - region threading - TODO rebase ++ ++ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ // Paper - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH ++ ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - region threading - move into own function TODO rebase ++ } + + // Paper start - rewrite chunk system + public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler; +@@ -446,81 +479,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper end + + // Paper start - optimise checkDespawn +- public final List playersAffectingSpawning = new java.util.ArrayList<>(); ++ // Paper - region threading + // Paper end - optimise checkDespawn + // Paper start - optimise get nearest players for entity AI +- @Override +- public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source, +- double centerX, double centerY, double centerZ) { +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; +- nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); +- +- if (nearby == null) { +- return null; +- } ++ // Paper - region threading + +- Object[] backingSet = nearby.getBackingSet(); +- +- double closestDistanceSquared = Double.MAX_VALUE; +- ServerPlayer closest = null; +- +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- Object _player = backingSet[i]; +- if (!(_player instanceof ServerPlayer)) { +- continue; +- } +- ServerPlayer player = (ServerPlayer)_player; +- +- double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ); +- if (distanceSquared < closestDistanceSquared && condition.test(source, player)) { +- closest = player; +- closestDistanceSquared = distanceSquared; +- } +- } +- +- return closest; +- } ++ // Paper - region threading + +- @Override +- public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) { +- return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ()); +- } +- +- @Override +- public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, +- double d0, double d1, double d2) { +- return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2); +- } +- +- @Override +- public List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) { +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; +- double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5; +- double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5; +- nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); +- +- List ret = new java.util.ArrayList<>(); +- +- if (nearby == null) { +- return ret; +- } +- +- Object[] backingSet = nearby.getBackingSet(); +- +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- Object _player = backingSet[i]; +- if (!(_player instanceof ServerPlayer)) { +- continue; +- } +- ServerPlayer player = (ServerPlayer)_player; +- +- if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) { +- ret.add(player); +- } +- } ++ // Paper - region threading + +- return ret; +- } ++ // Paper - region threading + // Paper end - optimise get nearest players for entity AI + + public final io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader(this); +@@ -565,6 +533,29 @@ public class ServerLevel extends Level implements WorldGenLevel { + }); + } + ++ // Paper start - regionised ticking ++ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); ++ public final io.papermc.paper.threadedregions.ThreadedRegioniser regioniser; ++ { ++ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegioniser<>( ++ 3*9, ++ (2.0 / 3.0), ++ 1, ++ 1, ++ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), ++ this, ++ this.tickRegions ++ ); ++ } ++ public final io.papermc.paper.threadedregions.RegionisedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionisedTaskQueue.WorldRegionTaskData(this); ++ public static final int WORLD_INIT_NOT_CHECKED = 0; ++ public static final int WORLD_INIT_CHECKING = 1; ++ public static final int WORLD_INIT_CHECKED = 2; ++ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); ++ public ChunkPos randomSpawnSelection; ++ ++ // Paper end - regionised ticking ++ + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // Holder holder = worlddimension.type(); // CraftBukkit - decompile error +@@ -574,13 +565,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.convertable = convertable_conversionsession; + this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); + // CraftBukkit end +- this.players = Lists.newArrayList(); +- this.entityTickList = new EntityTickList(); +- this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); +- this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); ++ this.players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Paper - region threading ++ //this.entityTickList = new EntityTickList(); // Paper - region threading ++ //this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Paper - moved to RegioniedWorldData ++ //this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Paper - moved to RegioniedWorldData + this.navigatingMobs = new ObjectOpenHashSet(); +- this.blockEvents = new ObjectLinkedOpenHashSet(); +- this.blockEventsToReschedule = new ArrayList(64); ++ //this.blockEvents = new ObjectLinkedOpenHashSet(); // Paper - moved to RegioniedWorldData ++ //this.blockEventsToReschedule = new ArrayList(64); // Paper - moved to RegioniedWorldData + this.dragonParts = new Int2ObjectOpenHashMap(); + this.tickTime = flag1; + this.server = minecraftserver; +@@ -619,7 +610,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + }); + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); + this.portalForcer = new PortalForcer(this); +- this.updateSkyBrightness(); ++ //this.updateSkyBrightness(); // Paper - region threading - delay until first tick + this.prepareWeather(); + this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize()); + this.raids = (Raids) this.getDataStorage().computeIfAbsent((nbttagcompound) -> { +@@ -647,8 +638,15 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system + this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system ++ this.updateTickData(); // Paper - region threading - make sure it is initialised before ticked + } + ++ // Paper start - region threading ++ public void updateTickData() { ++ this.tickData = new io.papermc.paper.threadedregions.RegionisedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); ++ } ++ // Paper end - region threading ++ + public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { + this.serverLevelData.setClearWeatherTime(clearDuration); + this.serverLevelData.setRainTime(rainDuration); +@@ -666,26 +664,20 @@ public class ServerLevel extends Level implements WorldGenLevel { + return this.structureManager; + } + +- public void tick(BooleanSupplier shouldKeepTicking) { +- // Paper start - optimise checkDespawn +- this.playersAffectingSpawning.clear(); +- for (ServerPlayer player : this.players) { +- if (net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { +- this.playersAffectingSpawning.add(player); +- } +- } +- // Paper end - optimise checkDespawn ++ public void tick(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Paper - regionised ticking ++ final io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = this.getCurrentWorldData(); // Paper - regionised ticking ++ // Paper - region threading + ProfilerFiller gameprofilerfiller = this.getProfiler(); + +- this.handlingTick = true; ++ regionisedWorldData.setHandlingTick(true); // Paper - regionised ticking + gameprofilerfiller.push("world border"); +- this.getWorldBorder().tick(); ++ if (region == null) this.getWorldBorder().tick(); // Paper - regionised ticking - moved into global tick + gameprofilerfiller.popPush("weather"); +- this.advanceWeatherCycle(); ++ if (region == null) this.advanceWeatherCycle(); + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + long j; + +- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { ++ if (region != null && this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { // Paper - region threading - TODO bring this back + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); +@@ -705,23 +697,23 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- this.updateSkyBrightness(); ++ if (region == null) this.updateSkyBrightness(); // Paper - region threading + this.tickTime(); + gameprofilerfiller.popPush("tickPending"); + timings.scheduledBlocks.startTiming(); // Paper + if (!this.isDebug()) { +- j = this.getGameTime(); ++ j = regionisedWorldData.getRedstoneGameTime(); // Paper - region threading + gameprofilerfiller.push("blockTicks"); +- this.blockTicks.tick(j, 65536, this::tickBlock); ++ regionisedWorldData.getBlockLevelTicks().tick(j, 65536, this::tickBlock); // Paper - region ticking + gameprofilerfiller.popPush("fluidTicks"); +- this.fluidTicks.tick(j, 65536, this::tickFluid); ++ regionisedWorldData.getFluidLevelTicks().tick(j, 65536, this::tickFluid); // Paper - region ticking + gameprofilerfiller.pop(); + } + timings.scheduledBlocks.stopTiming(); // Paper + + gameprofilerfiller.popPush("raid"); + this.timings.raids.startTiming(); // Paper - timings +- this.raids.tick(); ++ if (region == null) this.raids.tick(); // Paper - region threading - TODO fucking RAIDS + this.timings.raids.stopTiming(); // Paper - timings + gameprofilerfiller.popPush("chunkSource"); + this.timings.chunkProviderTick.startTiming(); // Paper - timings +@@ -731,7 +723,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + timings.doSounds.startTiming(); // Spigot + this.runBlockEvents(); + timings.doSounds.stopTiming(); // Spigot +- this.handlingTick = false; ++ regionisedWorldData.setHandlingTick(false); // Paper - regionised ticking + gameprofilerfiller.pop(); + boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players + +@@ -743,20 +735,30 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.push("entities"); + timings.tickEntities.startTiming(); // Spigot + if (this.dragonFight != null) { ++ if (io.papermc.paper.util.TickThread.isTickThreadFor(this, 0, 0)) { // Paper - region threading + gameprofilerfiller.push("dragonFight"); + this.dragonFight.tick(); + gameprofilerfiller.pop(); ++ } else { // Paper start - region threading ++ // try to load dragon fight ++ ChunkPos fightCenter = new ChunkPos(0, 0); ++ this.chunkSource.addTicketAtLevel( ++ TicketType.UNKNOWN, fightCenter, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ fightCenter ++ ); ++ } // Paper end - region threading + } + + org.spigotmc.ActivationRange.activateEntities(this); // Spigot + timings.entityTick.startTiming(); // Spigot +- this.entityTickList.forEach((entity) -> { ++ regionisedWorldData.forEachTickingEntity((entity) -> { // Paper - regionised ticking + if (!entity.isRemoved()) { + if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed + entity.discard(); + } else { + gameprofilerfiller.push("checkDespawn"); + entity.checkDespawn(); ++ if (entity.isRemoved()) return; // Paper - region threading - if we despawned, DON'T TICK IT! + gameprofilerfiller.pop(); + if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); +@@ -797,11 +799,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + + protected void tickTime() { + if (this.tickTime) { +- long i = this.levelData.getGameTime() + 1L; ++ io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = this.getCurrentWorldData(); // Paper - region threading ++ long i = regionisedWorldData.getRedstoneGameTime() + 1L; // Paper - region threading + +- this.serverLevelData.setGameTime(i); +- this.serverLevelData.getScheduledEvents().tick(this.server, i); +- if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ regionisedWorldData.setRedstoneGameTime(i); // Paper - region threading ++ if (false) this.serverLevelData.getScheduledEvents().tick(this.server, i); // Paper - region threading - TODO any way to bring this in? ++ if (false && this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Paper - region threading + this.setDayTime(this.levelData.getDayTime() + 1L); + } + +@@ -834,11 +837,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + }); + } + // Paper start - optimise random block ticking +- private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); +- private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); ++ private final ThreadLocal chunkTickMutablePosition = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Paper - region threading ++ private final ThreadLocal randomTickRandom = ThreadLocal.withInitial(() -> new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong())); // Paper - region threading + // Paper end + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { ++ io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = this.randomTickRandom.get(); // Paper - region threading + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); + int j = chunkcoordintpair.getMinBlockX(); +@@ -846,7 +850,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); +- final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change ++ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition.get(); // Paper - use mutable to reduce allocation rate, final to force compile fail on change // Paper - region threading + + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - disable thunder + blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper +@@ -941,7 +945,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + int yPos = (sectionIndex + minSection) << 4; + for (int a = 0; a < randomTickSpeed; ++a) { + int tickingBlocks = section.tickingList.size(); +- int index = this.randomTickRandom.nextInt(16 * 16 * 16); ++ int index = randomTickRandom.nextInt(16 * 16 * 16); // Paper - region threading + if (index >= tickingBlocks) { + continue; + } +@@ -955,7 +959,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); + BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); + +- iblockdata.randomTick(this, blockposition2, this.randomTickRandom); ++ iblockdata.randomTick(this, blockposition2, randomTickRandom); // Paper - region threading + // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). + // TODO CHECK ON UPDATE (ping the Canadian) + } +@@ -1009,7 +1013,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public boolean isHandlingTick() { +- return this.handlingTick; ++ return this.getCurrentWorldData().isHandlingTick(); // Paper - regionised ticking + } + + public boolean canSleepThroughNights() { +@@ -1041,6 +1045,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void updateSleepingPlayerList() { ++ if (true) return; // Paper - region threading - TODO figure this shit out + if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { + this.announceSleepStatus(); + } +@@ -1052,7 +1057,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + return this.server.getScoreboard(); + } + +- private void advanceWeatherCycle() { ++ public void advanceWeatherCycle() { // Paper - region threading - public + boolean flag = this.isRaining(); + + if (this.dimensionType().hasSkyLight()) { +@@ -1138,23 +1143,24 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel)); + } + // */ +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level == this) { +- ((ServerPlayer) this.players.get(idx)).tickWeather(); ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Paper - region threading ++ for (ServerPlayer player : players) { // Paper - region threading ++ if (player.level == this) { // Paper - region threading ++ player.tickWeather(); // Paper - region threading + } + } + + if (flag != this.isRaining()) { + // Only send weather packets to those affected +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level == this) { +- ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); ++ for (ServerPlayer player : players) { // Paper - region threading ++ if (player.level == this) { // Paper - region threading ++ player.setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); // Paper - region threading + } + } + } +- for (int idx = 0; idx < this.players.size(); ++idx) { +- if (((ServerPlayer) this.players.get(idx)).level == this) { +- ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); ++ for (ServerPlayer player : players) { // Paper - region threading ++ if (player.level == this) { // Paper - region threading ++ player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); // Paper - region threading + } + } + // CraftBukkit end +@@ -1218,7 +1224,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public void tickNonPassenger(Entity entity) { + // Paper start - log detailed entity tick information +- io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ io.papermc.paper.util.TickThread.ensureTickThread(entity, "Cannot tick an entity off-main"); // Paper - region threading + try { + if (currentlyTickingEntity.get() == null) { + currentlyTickingEntity.lazySet(entity); +@@ -1251,7 +1257,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (isActive) { // Paper - EAR 2 + TimingHistory.activatedEntityTicks++; + entity.tick(); +- entity.postTick(); // CraftBukkit ++ // Paper start - region threading ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) { ++ // removed from region while ticking ++ return; ++ } ++ if (entity.doPortalLogic()) { ++ // portalled ++ return; ++ } ++ // Paper end - region threading + } else { entity.inactiveTick(); } // Paper - EAR 2 + this.getProfiler().pop(); + } finally { timer.stopTiming(); } // Paper - timings +@@ -1274,7 +1289,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + private void tickPassenger(Entity vehicle, Entity passenger) { + if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) { +- if (passenger instanceof Player || this.entityTickList.contains(passenger)) { ++ if (passenger instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passenger)) { // Paper - region threading + // Paper - EAR 2 + final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); + co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper +@@ -1291,7 +1306,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper start - EAR 2 + if (isActive) { + passenger.rideTick(); +- passenger.postTick(); // CraftBukkit ++ // Paper start - region threading ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(passenger)) { ++ // removed from region while ticking ++ return; ++ } ++ if (passenger.doPortalLogic()) { ++ // portalled ++ return; ++ } ++ // Paper end - region threading + } else { + passenger.setDeltaMovement(Vec3.ZERO); + passenger.inactiveTick(); +@@ -1379,7 +1403,15 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper - rewrite chunk system - entity saving moved into ChunkHolder + + } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system ++ // Paper - move into saveLevelData() ++ } + ++ public void saveLevelData() { // Paper - region threading ++ if (this.dragonFight != null) { ++ this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit ++ } ++ // Paper start - region threading ++ // moved from save + // CraftBukkit start - moved from MinecraftServer.saveChunks + ServerLevel worldserver1 = this; + +@@ -1387,12 +1419,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save()); + this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + // CraftBukkit end +- } +- +- private void saveLevelData() { +- if (this.dragonFight != null) { +- this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit +- } ++ // Paper end - region threading + + this.getChunkSource().getDataStorage().save(); + } +@@ -1447,6 +1474,19 @@ public class ServerLevel extends Level implements WorldGenLevel { + return list; + } + ++ // Paper start - region threading ++ @Nullable ++ public ServerPlayer getRandomLocalPlayer() { ++ List list = this.getLocalPlayers(); ++ list = new java.util.ArrayList<>(list); ++ list.removeIf((ServerPlayer player) -> { ++ return !player.isAlive(); ++ }); ++ ++ return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); ++ } ++ // Paper end - region threading ++ + @Nullable + public ServerPlayer getRandomPlayer() { + List list = this.getPlayers(LivingEntity::isAlive); +@@ -1548,8 +1588,8 @@ public class ServerLevel extends Level implements WorldGenLevel { + } else { + if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added + // Paper start - capture all item additions to the world +- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { +- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); ++ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Paper - region threading ++ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Paper - region threading + return true; + } + // Paper end +@@ -1688,7 +1728,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @Override + public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { +- if (this.isUpdatingNavigations) { ++ if (false && this.isUpdatingNavigations) { // Paper - region threading + String s = "recursive call to sendBlockUpdated"; + + Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); +@@ -1701,7 +1741,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { + List list = new ObjectArrayList(); +- Iterator iterator = this.navigatingMobs.iterator(); ++ Iterator iterator = this.getCurrentWorldData().getNavigatingMobs(); // Paper - region threading + + while (iterator.hasNext()) { + // CraftBukkit start - fix SPIGOT-6362 +@@ -1724,7 +1764,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + try { +- this.isUpdatingNavigations = true; ++ //this.isUpdatingNavigations = true; // Paper - region threading + iterator = list.iterator(); + + while (iterator.hasNext()) { +@@ -1733,7 +1773,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + navigationabstract1.recomputePath(); + } + } finally { +- this.isUpdatingNavigations = false; ++ //this.isUpdatingNavigations = false; // Paper - region threading + } + + } +@@ -1742,23 +1782,23 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @Override + public void updateNeighborsAt(BlockPos pos, Block sourceBlock) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); ++ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Paper - region threading ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); // Paper - region threading + } + + @Override + public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) { +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); // Paper - region threading + } + + @Override + public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) { +- this.neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); // Paper - region threading + } + + @Override + public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { +- this.neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); // Paper - region threading + } + + @Override +@@ -1784,7 +1824,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + explosion.clearToBlow(); + } + +- Iterator iterator = this.players.iterator(); ++ Iterator iterator = this.getLocalPlayers().iterator(); // Paper - region thraeding + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +@@ -1799,25 +1839,28 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @Override + public void blockEvent(BlockPos pos, Block block, int type, int data) { +- this.blockEvents.add(new BlockEventData(pos, block, type, data)); ++ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, type, data)); // Paper - regionised ticking + } + + private void runBlockEvents() { +- this.blockEventsToReschedule.clear(); ++ List blockEventsToReschedule = new ArrayList<>(64); // Paper - regionised ticking + +- while (!this.blockEvents.isEmpty()) { +- BlockEventData blockactiondata = (BlockEventData) this.blockEvents.removeFirst(); ++ // Paper start - regionised ticking ++ io.papermc.paper.threadedregions.RegionisedWorldData worldRegionData = this.getCurrentWorldData(); ++ BlockEventData blockactiondata; ++ while ((blockactiondata = worldRegionData.removeFirstBlockEvent()) != null) { ++ // Paper end - regionised ticking + + if (this.shouldTickBlocksAt(blockactiondata.pos())) { + if (this.doBlockEvent(blockactiondata)) { + this.server.getPlayerList().broadcast((Player) null, (double) blockactiondata.pos().getX(), (double) blockactiondata.pos().getY(), (double) blockactiondata.pos().getZ(), 64.0D, this.dimension(), new ClientboundBlockEventPacket(blockactiondata.pos(), blockactiondata.block(), blockactiondata.paramA(), blockactiondata.paramB())); + } + } else { +- this.blockEventsToReschedule.add(blockactiondata); ++ blockEventsToReschedule.add(blockactiondata); // Paper - regionised ticking + } + } + +- this.blockEvents.addAll(this.blockEventsToReschedule); ++ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Paper - regionised ticking + } + + private boolean doBlockEvent(BlockEventData event) { +@@ -1828,12 +1871,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @Override + public LevelTicks getBlockTicks() { +- return this.blockTicks; ++ return this.getCurrentWorldData().getBlockLevelTicks(); // Paper - region ticking + } + + @Override + public LevelTicks getFluidTicks() { +- return this.fluidTicks; ++ return this.getCurrentWorldData().getFluidLevelTicks(); // Paper - region ticking + } + + @Nonnull +@@ -1857,7 +1900,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public int sendParticles(ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { + // Paper start - Particle API Expansion +- return sendParticles(players, sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); ++ return sendParticles(this.getLocalPlayers(), sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); // Paper - region threading + } + public int sendParticles(List receivers, ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { + // Paper end +@@ -1910,7 +1953,14 @@ public class ServerLevel extends Level implements WorldGenLevel { + public Entity getEntityOrPart(int id) { + Entity entity = (Entity) this.getEntities().get(id); + +- return entity != null ? entity : (Entity) this.dragonParts.get(id); ++ // Paper start - region threading ++ if (entity != null) { ++ return entity; ++ } ++ synchronized (this.dragonParts) { ++ return this.dragonParts.get(id); ++ } ++ // Paper end - region threading + } + + @Nullable +@@ -1918,6 +1968,61 @@ public class ServerLevel extends Level implements WorldGenLevel { + return (Entity) this.getEntities().get(uuid); + } + ++ // Paper start - region threading ++ private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong(); ++ ++ private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) { ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded = ++ this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion; ++ if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) { ++ return loadedCompletion.chunk(); ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) { ++ if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) { ++ throw new IllegalArgumentException("Status: " + status.getName()); ++ } ++ ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status); ++ if (loaded != null) { ++ return loaded; ++ } ++ ++ Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement()); ++ int ticketLevel = 33 + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status); ++ this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId ++ ); ++ this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); ++ ++ this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING); ++ ++ // we could do a simple spinwait here, since we do not need to process tasks while performing this load ++ // but we process tasks only because it's a better use of the time spent ++ this.chunkSource.mainThreadProcessor.managedBlock(() -> { ++ return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null; ++ }); ++ ++ loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status); ++ if (loaded == null) { ++ throw new IllegalStateException("Expected chunk to be loaded for status " + status); ++ } ++ ++ // let the next process ticket updates call pick this up ++ this.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdate( ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.TicketOperation.removeOp( ++ chunkX, chunkZ, TicketType.NON_FULL_SYNC_LOAD, ticketLevel, ticketId ++ ) ++ ); ++ ++ return loaded; ++ } ++ // Paper end - region threading ++ + @Nullable + public BlockPos findNearestMapStructure(TagKey structureTag, BlockPos pos, int radius, boolean skipReferencedStructures) { + if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit +@@ -2082,7 +2187,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (forced) { + flag1 = forcedchunk.getChunks().add(k); + if (flag1) { +- this.getChunk(x, z); ++ //this.getChunk(x, z); // Paper - region threading - we must let the chunk load asynchronously + } + } else { + flag1 = forcedchunk.getChunks().remove(k); +@@ -2110,13 +2215,18 @@ public class ServerLevel extends Level implements WorldGenLevel { + BlockPos blockposition1 = pos.immutable(); + + optional.ifPresent((holder) -> { +- this.getServer().execute(() -> { ++ Runnable run = () -> { // Paper - region threading + this.getPoiManager().remove(blockposition1); + DebugPackets.sendPoiRemovedPacket(this, blockposition1); +- }); ++ }; // Paper - region threading ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run ++ ); ++ // Paper end - region threading + }); + optional1.ifPresent((holder) -> { +- this.getServer().execute(() -> { ++ Runnable run = () -> { // Paper - region threading + // Paper start + if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) { + this.getPoiManager().remove(blockposition1); +@@ -2124,7 +2234,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper end + this.getPoiManager().add(blockposition1, holder); + DebugPackets.sendPoiAddedPacket(this, blockposition1); +- }); ++ }; // Paper - region threading ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run ++ ); ++ // Paper end - region threading + }); + } + } +@@ -2171,7 +2286,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + BufferedWriter bufferedwriter = Files.newBufferedWriter(path.resolve("stats.txt")); + + try { +- bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); ++ //bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); // Paper - region threading + NaturalSpawner.SpawnState spawnercreature_d = this.getChunkSource().getLastSpawnState(); + + if (spawnercreature_d != null) { +@@ -2185,7 +2300,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system +- bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); ++ //bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Paper - region threading + bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); + bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); + bufferedwriter.write("distance_manager: " + playerchunkmap.getDistanceManager().getDebugStatus() + "\n"); +@@ -2331,7 +2446,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + private void dumpBlockEntityTickers(Writer writer) throws IOException { + CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(writer); +- Iterator iterator = this.blockEntityTickers.iterator(); ++ Iterator iterator = null; // Paper - region threading + + while (iterator.hasNext()) { + TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next(); +@@ -2344,7 +2459,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @VisibleForTesting + public void clearBlockEvents(BoundingBox box) { +- this.blockEvents.removeIf((blockactiondata) -> { ++ this.getCurrentWorldData().removeIfBlockEvents((blockactiondata) -> { // Paper - regionised ticking + return box.isInside(blockactiondata.pos()); + }); + } +@@ -2353,7 +2468,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public void blockUpdated(BlockPos pos, Block block) { + if (!this.isDebug()) { + // CraftBukkit start +- if (populating) { ++ if (this.getCurrentWorldData().populating) { // Paper - region threading + return; + } + // CraftBukkit end +@@ -2396,9 +2511,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @VisibleForTesting + public String getWatchdogStats() { +- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system +- return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); +- }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); ++ return "region threading"; // Paper - region threading + } + + private static String getTypeCount(Iterable items, Function classifier) { +@@ -2431,6 +2544,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + public static void makeObsidianPlatform(ServerLevel worldserver, Entity entity) { + // CraftBukkit end + BlockPos blockposition = ServerLevel.END_SPAWN_POINT; ++ // Paper start - region threading ++ makeObsidianPlatform(worldserver, entity, blockposition); ++ } ++ ++ public static void makeObsidianPlatform(ServerLevel worldserver, Entity entity, BlockPos blockposition) { ++ // Paper end - region threading + int i = blockposition.getX(); + int j = blockposition.getY() - 2; + int k = blockposition.getZ(); +@@ -2443,11 +2562,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + BlockPos.betweenClosed(i - 2, j, k - 2, i + 2, j, k + 2).forEach((blockposition1) -> { + blockList.setBlock(blockposition1, Blocks.OBSIDIAN.defaultBlockState(), 3); + }); +- org.bukkit.World bworld = worldserver.getWorld(); +- org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((List) (List) blockList.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); +- +- worldserver.getCraftServer().getPluginManager().callEvent(portalEvent); +- if (!portalEvent.isCancelled()) { ++ if (true) { // Paper - region threading + blockList.updateList(); + } + // CraftBukkit end +@@ -2468,13 +2583,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void startTickingChunk(LevelChunk chunk) { +- chunk.unpackTicks(this.getLevelData().getGameTime()); ++ chunk.unpackTicks(this.getRedstoneGameTime()); // Paper - region threading + } + + public void onStructureStartsAvailable(ChunkAccess chunk) { +- this.server.execute(() -> { +- this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); +- }); ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueChunkTask( ++ this, chunk.getPos().x, chunk.getPos().z, () -> { ++ this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); ++ } ++ ); ++ // Paper end - region threading + } + + @Override +@@ -2496,7 +2615,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Paper end - rewrite chunk system + } + +- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { ++ public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Paper - region threaded - make public + // Paper start - optimize is ticking ready type functions + io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos); + // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded +@@ -2544,16 +2663,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + public void onCreated(Entity entity) {} + + public void onDestroyed(Entity entity) { +- ServerLevel.this.getScoreboard().entityRemoved(entity); ++ // ServerLevel.this.getScoreboard().entityRemoved(entity); // Paper - region threading + } + + public void onTickingStart(Entity entity) { + if (entity instanceof net.minecraft.world.entity.Marker) return; // Paper - Don't tick markers +- ServerLevel.this.entityTickList.add(entity); ++ ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Paper - region threading + } + + public void onTickingEnd(Entity entity) { +- ServerLevel.this.entityTickList.remove(entity); ++ ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Paper - region threading + // Paper start - Reset pearls when they stop being ticked + if (paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { + pearl.cachedOwner = null; +@@ -2581,7 +2700,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); + } + +- ServerLevel.this.navigatingMobs.add(entityinsentient); ++ ServerLevel.this.getCurrentWorldData().addNavigatingMob(entityinsentient); // Paper - region threading + } + + if (entity instanceof EnderDragon) { +@@ -2592,7 +2711,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + for (int j = 0; j < i; ++j) { + EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + ++ synchronized (ServerLevel.this.dragonParts) { // Paper - region threading + ServerLevel.this.dragonParts.put(entitycomplexpart.getId(), entitycomplexpart); ++ } // Paper - region threading + } + } + +@@ -2666,7 +2787,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); + } + +- ServerLevel.this.navigatingMobs.remove(entityinsentient); ++ ServerLevel.this.getCurrentWorldData().removeNavigatingMob(entityinsentient); // Paper - region threading + } + + if (entity instanceof EnderDragon) { +@@ -2677,13 +2798,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + for (int j = 0; j < i; ++j) { + EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + ++ synchronized (ServerLevel.this.dragonParts) { // Paper - region threading + ServerLevel.this.dragonParts.remove(entitycomplexpart.getId()); ++ } // Paper - region threading + } + } + + entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); + // CraftBukkit start + entity.valid = false; ++ // Paper - region threading - TODO THIS SHIT + if (!(entity instanceof ServerPlayer)) { + for (ServerPlayer player : ServerLevel.this.players) { + player.getBukkitEntity().onEntityRemove(entity); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 869daafbc236b3ff63f878e5fe28427fde75afe5..ef954194ec674c81b67227110e13cddb237c8de5 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -181,7 +181,7 @@ import org.bukkit.inventory.MainHand; + public class ServerPlayer extends Player { + + private static final Logger LOGGER = LogUtils.getLogger(); +- public long lastSave = MinecraftServer.currentTick; // Paper ++ public long lastSave = Long.MIN_VALUE; // Paper // Paper - threaded regions + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + public ServerGamePacketListenerImpl connection; +@@ -311,6 +311,9 @@ public class ServerPlayer extends Player { + }); + } + ++ // Paper start - region threading ++ // Paper end - region threading ++ + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); + this.chatVisibility = ChatVisiblity.FULL; +@@ -450,11 +453,11 @@ public class ServerPlayer extends Player { + } + // CraftBukkit end + +- public void fudgeSpawnLocation(ServerLevel world) { +- BlockPos blockposition = world.getSharedSpawnPos(); ++ public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { // Paper - region threading ++ BlockPos blockposition = world.getSharedSpawnPos(); final BlockPos spawnPos = blockposition; // Paper - region threading + + if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit +- int i = Math.max(0, this.server.getSpawnRadius(world)); ++ int i = Math.max(0, MinecraftServer.getServer().getSpawnRadius(world)); // Paper - region threading + int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double) blockposition.getX(), (double) blockposition.getZ())); + + if (j < i) { +@@ -468,33 +471,72 @@ public class ServerPlayer extends Player { + long k = (long) (i * 2 + 1); + long l = k * k; + int i1 = l > 2147483647L ? Integer.MAX_VALUE : (int) l; +- int j1 = this.getCoprime(i1); ++ int j1 = getCoprime(i1); // Paper - region threading + int k1 = RandomSource.create().nextInt(i1); + +- for (int l1 = 0; l1 < i1; ++l1) { +- int i2 = (k1 + j1 * l1) % i1; +- int j2 = i2 % (i * 2 + 1); +- int k2 = i2 / (i * 2 + 1); +- BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(world, blockposition.getX() + j2 - i, blockposition.getZ() + k2 - i); +- +- if (blockposition1 != null) { +- this.moveTo(blockposition1, 0.0F, 0.0F); +- if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now +- break; +- } ++ // Paper start - region threading ++ int[] l1 = new int[1]; ++ final int finalI = i; ++ Runnable attempt = new Runnable() { ++ @Override ++ public void run() { ++ int i2 = (k1 + j1 * l1[0]) % i1; ++ int j2 = i2 % (finalI * 2 + 1); ++ int k2 = i2 / (finalI * 2 + 1); ++ int x = blockposition.getX() + j2 - finalI; ++ int z = blockposition.getZ() + k2 - finalI; ++ ++ world.loadChunksForMoveAsync(player.getBoundingBoxAt(x + 0.5, 0, z + 0.5), ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (c) -> { ++ BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(world, x, z); ++ if (blockposition1 != null) { ++ AABB aabb = player.getBoundingBoxAt(blockposition1.getX() + 0.5, blockposition1.getY(), blockposition1.getZ() + 0.5); ++ if (world.noCollision(player, aabb, true)) { // Paper - make sure this loads chunks, we default to NOT loading now ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(blockposition1), world.levelData.getSpawnAngle(), 0.0f)); ++ return; ++ } ++ } ++ if (++l1[0] >= i1) { ++ LOGGER.warn("Found no spawn in radius for player " + player.getName() + ", selecting set spawn point " + spawnPos + " in world '" + world.getWorld().getKey() + "'"); ++ // if we return null, then no chunks may be loaded. but this call requires to return a location with ++ // loaded chunks, so we need to return something (vanilla does not do this logic, it assumes ++ // something is returned always) ++ // we can just return the set spawn position ++ world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5), ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (c0) -> { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(spawnPos), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ); ++ return; ++ } else { ++ this.run(); ++ } ++ } ++ ); + } +- } ++ }; ++ attempt.run(); ++ // Paper end - region threading + } else { +- this.moveTo(blockposition, 0.0F, 0.0F); +- +- while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now +- this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); +- } ++ // Paper start - region threading ++ world.loadChunksForMoveAsync(player.getBoundingBoxAt(blockposition.getX() + 0.5, blockposition.getY(), blockposition.getZ() + 0.5), ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (c) -> { ++ BlockPos ret = blockposition; ++ while (!world.noCollision(player, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5), true) && ret.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now ++ ret = ret.above(); ++ } ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ); ++ // Paper end - region threading + } + + } + +- private int getCoprime(int horizontalSpawnArea) { ++ private static int getCoprime(int horizontalSpawnArea) { // Paper - region threading - not static + return horizontalSpawnArea <= 16 ? horizontalSpawnArea - 1 : 17; + } + +@@ -1147,6 +1189,338 @@ public class ServerPlayer extends Player { + } + } + ++ // Paper start - region threading ++ /** ++ * Teleport flag indicating that the player is to be respawned, expected to only be used ++ * internally for {@link #respawn(java.util.function.Consumer)}. ++ */ ++ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; ++ /** ++ * Teleport flag indicating the player should be placed at the highest y-value that ++ * provides no collisions for the player's bounding box. Note that this setting ++ * does not imply {@link Entity#TELEPORT_FLAG_LOAD_CHUNK}, so it may ++ * sync load chunks unless the load chunk flag is provided. ++ */ ++ public static final long TELEPORT_FLAGS_AVOID_SUFFOCATION = Long.MIN_VALUE >>> 1; ++ ++ private void avoidSuffocation() { ++ while (!this.getLevel().noCollision(this, this.getBoundingBox(), true) && this.getY() < (double)this.getLevel().getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now ++ this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); ++ } ++ } ++ ++ public void exitEndCredits() { ++ if (!this.wonGame) { ++ // not in the end credits anymore ++ return; ++ } ++ this.wonGame = false; ++ ++ this.respawn((player) -> { ++ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); ++ }, true); ++ } ++ ++ public void respawn(java.util.function.Consumer respawnComplete) { ++ this.respawn(respawnComplete, false); ++ } ++ ++ private void respawn(java.util.function.Consumer respawnComplete, boolean alive) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); ++ ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ ++ if (alive != this.isAlive()) { ++ throw new IllegalStateException("isAlive expected = " + alive); ++ } ++ ++ if (this.isVehicle() || this.isPassenger()) { ++ throw new IllegalStateException("Dead player should not be a vehicle or passenger"); ++ } ++ ++ ServerLevel origin = this.getLevel(); ++ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); ++ ++ // modified based off PlayerList#respawn ++ ++ EntityTreeNode passengerTree = this.makePassengerTree(); ++ ++ this.isChangingDimension = true; ++ // must be manually removed from connections ++ this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); ++ ++ BlockPos respawnPos = this.getRespawnPosition(); ++ float respawnAngle = this.getRespawnAngle(); ++ boolean isRespawnForced = this.isRespawnForced(); ++ ++ ca.spottedleaf.concurrentutil.completable.Completable spawnPosComplete = ++ new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ boolean[] usedRespawnAnchor = new boolean[1]; ++ ++ // set up post spawn location logic ++ spawnPosComplete.addWaiter((spawnLoc, throwable) -> { ++ // reset player if needed ++ if (!alive) { ++ ServerPlayer.this.reset(); ++ } ++ ++ // update pos and velocity ++ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); ++ ServerPlayer.this.setYRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setXRot(spawnLoc.getPitch()); ++ ServerPlayer.this.setDeltaMovement(Vec3.ZERO); ++ // placeInAsync will update the world ++ ++ this.placeInAsync( ++ origin, ++ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks ++ // stay loaded for a bit with the teleport ticket ++ ((CraftWorld)spawnLoc.getWorld()).getHandle(), ++ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN | TELEPORT_FLAGS_AVOID_SUFFOCATION, ++ passengerTree, // note: we expect this to just be the player, no passengers ++ (entity) -> { ++ // now the player is in the world, and can receive sound ++ if (usedRespawnAnchor[0]) { ++ ServerPlayer.this.connection.send( ++ new ClientboundSoundPacket( ++ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, ++ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), ++ 1.0F, 1.0F, ServerPlayer.this.getLevel().getRandom().nextLong() ++ ) ++ ); ++ } ++ // now the respawn logic is complete ++ ++ // last, call the function callback ++ if (respawnComplete != null) { ++ respawnComplete.accept(ServerPlayer.this); ++ } ++ } ++ ); ++ }); ++ ++ // find and modify respawn block state ++ if (respawnWorld == null || respawnPos == null) { ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ } else { ++ // load chunk for block ++ // give at least 1 radius of loaded chunks so that we do not sync load anything ++ int radiusBlocks = 16; ++ respawnWorld.loadChunksAsync(respawnPos, radiusBlocks, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (chunks) -> { ++ Vec3 spawnPos = findRespawnPositionAndUseSpawnBlock( ++ respawnWorld, respawnPos, respawnAngle, isRespawnForced, alive ++ ).orElse(null); ++ if (spawnPos == null) { ++ // no spawn ++ ServerPlayer.this.connection.send( ++ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) ++ ); ++ ServerPlayer.this.setRespawnPosition( ++ null, null, 0f, false, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ return; ++ } ++ ++ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(Blocks.RESPAWN_ANCHOR); ++ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); ++ usedRespawnAnchor[0] = !alive && isRespawnAnchor; ++ ++ // determine angle ++ float locAngle; ++ if (!isBed && !isRespawnAnchor) { ++ // something else ++ locAngle = respawnAngle; ++ } else { ++ // select angle in direction of the difference applied to respawn pos? ++ Vec3 vec3d1 = Vec3.atBottomCenterOf(respawnPos).subtract(spawnPos).normalize(); ++ ++ locAngle = (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D); ++ } ++ ServerPlayer.this.setRespawnPosition( ++ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ ++ // finished now, pass the location on ++ spawnPosComplete.complete( ++ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos, locAngle, 0.0f) ++ ); ++ return; ++ } ++ ); ++ } ++ } ++ ++ @Override ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (speedDirectionUpdate != null) { ++ this.setDeltaMovement(speedDirectionUpdate.normalize().scale(this.getDeltaMovement().length())); ++ } ++ this.connection.internalTeleport(pos.x, pos.y, pos.z, this.getYRot(), this.getXRot(), java.util.Collections.emptySet(), false); ++ this.connection.resetPosition(); ++ } ++ ++ @Override ++ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { ++ // must be manually removed from connections ++ this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ this.getLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); ++ ++ this.transform(pos, yaw, pitch, speedDirectionUpdate); ++ ++ return this; ++ } ++ ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopUsingItem(); ++ } ++ ++ @Override ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.getLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { ++ this.avoidSuffocation(); ++ } ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), ++ java.util.Collections.emptySet(), treeNode.parent == null ++ ); ++ this.connection.resetPosition(); ++ ++ this.postChangeDimension(); ++ } else { ++ // Modelled after PlayerList#respawn ++ ++ // We avoid checking for disconnection here, which means we do not have to add/remove from ++ // the player list here. We can let this be properly handled by the connection handler ++ ++ // pre-add logic ++ PlayerList playerlist = this.server.getPlayerList(); ++ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); ++ this.connection.send( ++ new ClientboundRespawnPacket( ++ destination.dimensionTypeId(), destination.dimension(), ++ BiomeManager.obfuscateSeed(destination.getSeed()), ++ this.gameMode.getGameModeForPlayer(), ++ this.gameMode.getPreviousGameModeForPlayer(), ++ destination.isDebug(), destination.isFlat(), ++ // if we do not want to respawn, we aren't dead ++ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0, ++ this.getLastDeathLocation() ++ ) ++ ); ++ // don't bother with the chunk cache radius and simulation distance packets, they are handled ++ // by the chunk loader ++ this.spawnIn(destination); // important that destination != null ++ // we can delay teleport until later, the player position is already set up at the target ++ this.setShiftKeyDown(false); ++ ++ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( ++ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() ++ )); ++ this.connection.send(new ClientboundChangeDifficultyPacket( ++ worlddata.getDifficulty(), worlddata.isDifficultyLocked() ++ )); ++ this.connection.send(new ClientboundSetExperiencePacket( ++ this.experienceProgress, this.totalExperience, this.experienceLevel ++ )); ++ ++ playerlist.sendLevelInfo(this, destination); ++ playerlist.sendPlayerPermissionLevel(this); ++ ++ // regular world add logic ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.getLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { ++ this.avoidSuffocation(); ++ } ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), ++ java.util.Collections.emptySet(), treeNode.parent == null ++ ); ++ this.connection.resetPosition(); ++ ++ // delay callback until after post add logic ++ ++ // post add logic ++ ++ // "Added from changeDimension" ++ this.setHealth(this.getHealth()); ++ playerlist.sendAllPlayerInfo(this); ++ this.onUpdateAbilities(); ++ for (MobEffectInstance mobEffect : this.getActiveEffects()) { ++ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect)); ++ } ++ ++ this.triggerDimensionChangeTriggers(originWorld); ++ ++ // finished ++ ++ this.postChangeDimension(); ++ } ++ } ++ ++ @Override ++ public boolean endPortalLogicAsync() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ if (this.getLevel().getTypeKey() == LevelStem.END) { ++ if (!this.canPortalAsync(false)) { ++ return false; ++ } ++ this.wonGame = true; ++ // TODO is there a better solution to this that DOESN'T skip the credits? ++ this.seenCredits = true; ++ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, this.seenCredits ? 0.0F : 1.0F)); ++ this.exitEndCredits(); ++ return true; ++ } else { ++ return super.endPortalLogicAsync(); ++ } ++ } ++ ++ @Override ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ super.prePortalLogic(origin, destination, type); ++ if (origin.getTypeKey() == LevelStem.OVERWORLD && destination.getTypeKey() == LevelStem.NETHER) { ++ this.enteredNetherPosition = this.position(); ++ } ++ } ++ // Paper end - region threading ++ + @Nullable + @Override + public Entity changeDimension(ServerLevel destination) { +@@ -2098,6 +2472,12 @@ public class ServerPlayer extends Player { + + if (entity1 == entity) return; // new spec target is the current spec target + ++ // Paper start - region threading ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) { ++ return; ++ } ++ // Paper end - region threading ++ + if (entity == this) { + com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent playerStopSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity()); + +@@ -2132,7 +2512,7 @@ public class ServerPlayer extends Player { + this.getBukkitEntity().teleport(new Location(entity.getCommandSenderWorld().getWorld(), entity.getX(), entity.getY(), entity.getZ(), this.getYRot(), this.getXRot()), TeleportCause.SPECTATE); // Correctly handle cross-world entities from api calls by using CB teleport + + // Make sure we're tracking the entity before sending +- ChunkMap.TrackedEntity tracker = ((ServerLevel)entity.level).getChunkSource().chunkMap.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity tracker = entity.tracker; // Paper - region threading + if (tracker != null) { // dumb plugins... + tracker.updatePlayer(this); + } +@@ -2567,7 +2947,7 @@ public class ServerPlayer extends Player { + this.experienceLevel = this.newLevel; + this.totalExperience = this.newTotalExp; + this.experienceProgress = 0; +- this.deathTime = 0; ++ this.deathTime = 0; this.broadcastedDeath = false; // Paper - region threading + this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent + this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); + this.effectsDirty = true; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 58b093bb1de78ee3b3b2ea364aa50474883f443a..dd0f309f725a6548057d5975602f914b47c96ed9 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -123,7 +123,7 @@ public class ServerPlayerGameMode { + } + + public void tick() { +- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; ++ ++this.gameTicks; // CraftBukkit; // Paper - region threading + BlockState iblockdata; + + if (this.hasDelayedDestroy) { +@@ -408,7 +408,7 @@ public class ServerPlayerGameMode { + } else { + // CraftBukkit start + org.bukkit.block.BlockState state = bblock.getState(); +- level.captureDrops = new ArrayList<>(); ++ level.getCurrentWorldData().captureDrops = new ArrayList<>(); // Paper - region threading + // CraftBukkit end + block.playerWillDestroy(this.level, pos, iblockdata, this.player); + boolean flag = this.level.removeBlock(pos, false); +@@ -436,8 +436,8 @@ public class ServerPlayerGameMode { + // return true; // CraftBukkit + } + // CraftBukkit start +- java.util.List itemsToDrop = level.captureDrops; // Paper - store current list +- level.captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff ++ java.util.List itemsToDrop = level.getCurrentWorldData().captureDrops; // Paper - store current list // Paper - region threading ++ level.getCurrentWorldData().captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff // Paper - region threading + if (event.isDropItems()) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - use stored ref + } +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index 660693c6dc0ef86f4013df980b6d0c11c03e46cd..62ff993a69035c154c7b492f0e035c1dc485a326 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -98,10 +98,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + this.chunkMap.level.chunkTaskScheduler.lightExecutor.queueRunnable(() -> { // Paper - rewrite chunk system + this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { + chunkLightCallback.accept(chunkPos); +- ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { ++ Runnable run = () -> { // Paper - region threading + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); +- }); ++ }; // Paper - region threading ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueChunkTask( ++ (ServerLevel)this.theLightEngine.getWorld(), chunkPos.x, chunkPos.z, run ++ ); ++ // Paper end - region threading + }, onComplete); + }); + this.tryScheduleUpdate(); +@@ -109,7 +114,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + return totalChunks; + } + +- private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); ++ //private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); // Paper - region threading + + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { + final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); +@@ -128,11 +133,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + return; + } + +- if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(world, chunkX, chunkZ)) { // Paper - region threading + // ticket logic is not safe to run off-main, re-schedule +- world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { ++ Runnable run = () -> { // Paper - region threading + this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); +- }); ++ }; // Paper - region threading ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, run ++ ); ++ // Paper end - region threading + return; + } + +@@ -145,22 +155,28 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + return; + } + +- final int references = this.chunksBeingWorkedOn.addTo(key, 1); ++ final int references = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.addTo(key, 1); // Paper - region threading + if (references == 0) { + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } + +- updateFuture.thenAcceptAsync((final Void ignore) -> { +- final int newReferences = this.chunksBeingWorkedOn.get(key); +- if (newReferences == 1) { +- this.chunksBeingWorkedOn.remove(key); +- final ChunkPos pos = new ChunkPos(chunkX, chunkZ); +- world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); +- } else { +- this.chunksBeingWorkedOn.put(key, newReferences - 1); +- } +- }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { ++ // Paper start - region threading ++ updateFuture.thenAccept((final Void ignore) -> { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ this.chunkMap.level, chunkX, chunkZ, () -> { ++ final int newReferences = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.get(key); ++ if (newReferences == 1) { ++ this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.remove(key); ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); ++ } else { ++ this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.put(key, newReferences - 1); ++ } ++ } ++ ); ++ }).whenComplete((final Void ignore, final Throwable thr) -> { ++ // Paper end - region threading + if (thr != null) { + LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); + } +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 97d1ff2af23bac14e67bca5896843325aaa5bfc1..f3d874af5e6196a8c29069220407fcb53d4317c2 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -35,6 +35,12 @@ public class TicketType { + public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); + public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); + // Paper end - rewrite chunk system ++ // Paper start - region threading ++ public static final TicketType LOGIN = create("login", (u1, u2) -> 0, 20); ++ public static final TicketType DELAYED = create("delay", (u1, u2) -> 0, 5); ++ public static final TicketType END_GATEWAY_EXIT_SEARCH = create("end_gateway_exit_search", Long::compareTo); ++ public static final TicketType NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo); ++ // Paper end - region threading + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 877498729c66de9aa6a27c9148f7494d7895615c..28cb049980be93c0ae7d613b82ef5232b15f3759 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -84,6 +84,13 @@ public class WorldGenRegion implements WorldGenLevel { + private final AtomicLong subTickCount = new AtomicLong(); + private static final ResourceLocation WORLDGEN_REGION_RANDOM = new ResourceLocation("worldgen_region_random"); + ++ // Paper start - region threading ++ @Override ++ public StructureManager structureManager() { ++ return this.structureManager; ++ } ++ // Paper end - region threading ++ + public WorldGenRegion(ServerLevel world, List chunks, ChunkStatus status, int placementRadius) { + this.generatingStatus = status; + this.writeRadiusCutoff = placementRadius; +diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +index abcc3266d18f34d160eac87fdea153dce24c60b8..1a2d38b16a74f7e2698c0426a8c4503e528c68b1 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java ++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +@@ -155,10 +155,13 @@ public class ServerConnectionListener { + // Paper end + + // ServerConnectionListener.this.connections.add((Connection) object); // CraftBukkit - decompile error +- pending.add((Connection) object); // Paper ++ // Paper - connection fixes - move down + channel.pipeline().addLast("packet_handler", (ChannelHandler) object); + ((Connection) object).setListener(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object)); + io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper ++ // Paper start - regionised threading ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addConnection((Connection)object); ++ // Paper end - regionised threading + } + }).group((EventLoopGroup) lazyinitvar.get()).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper + } +@@ -217,7 +220,7 @@ public class ServerConnectionListener { + // Spigot Start + this.addPending(); // Paper + // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order +- if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 ) ++ if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0 ) // Paper - region threading + { + Collections.shuffle( this.connections ); + } +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 3472f7f9b98d6d9c9f6465872803ef17fa67486d..56dc95a795e7742cb15ad23f0eb3a257cab45ad4 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -320,10 +320,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + private final org.bukkit.craftbukkit.CraftServer cserver; + public boolean processedDisconnect; +- private int lastTick = MinecraftServer.currentTick; ++ private long lastTick = Util.getMillis() / 50L; // Paper - region threading + private int allowedPlayerTicks = 1; +- private int lastDropTick = MinecraftServer.currentTick; +- private int lastBookTick = MinecraftServer.currentTick; ++ private long lastDropTick = Util.getMillis() / 50L; // Paper - region threading ++ private long lastBookTick = Util.getMillis() / 50L; // Paper - region threading + private int dropCount = 0; + + // Get position of last block hit for BlockDamageLevel.STOPPED +@@ -340,8 +340,40 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // CraftBukkit end + ++ // Paper start - region threading ++ public net.minecraft.world.level.ChunkPos disconnectPos; ++ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); ++ public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); ++ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); ++ ++ private void checkKeepAlive() { ++ long currentTime = Util.getMillis(); ++ long elapsedTime = currentTime - this.keepAliveTime; ++ ++ if (this.keepAlivePending) { ++ if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected ++ ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info ++ this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause ++ } ++ } else { ++ if (elapsedTime >= 15000L) { // 15 seconds ++ this.keepAlivePending = true; ++ this.keepAliveTime = currentTime; ++ this.keepAliveChallenge = currentTime; ++ this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge)); ++ } ++ } ++ } ++ // Paper end - region threading ++ + @Override + public void tick() { ++ // Paper start - region threading ++ this.checkKeepAlive(); ++ if (this.player.wonGame) { ++ return; ++ } ++ // Paper end - region threading + if (this.ackBlockChangesUpTo > -1) { + this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); + this.ackBlockChangesUpTo = -1; +@@ -393,22 +425,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.server.getProfiler().push("keepAlive"); + // Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings + // This should effectively place the keepalive handling back to "as it was" before 1.12.2 +- long currentTime = Util.getMillis(); +- long elapsedTime = currentTime - this.keepAliveTime; +- +- if (this.keepAlivePending) { +- if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected +- ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info +- this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause +- } +- } else { +- if (elapsedTime >= 15000L) { // 15 seconds +- this.keepAlivePending = true; +- this.keepAliveTime = currentTime; +- this.keepAliveChallenge = currentTime; +- this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge)); +- } +- } ++ // Paper - region threading - move to own method above + // Paper end + + this.server.getProfiler().pop(); +@@ -477,24 +494,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (this.processedDisconnect) { + return; + } +- if (!this.cserver.isPrimaryThread()) { +- Waitable waitable = new Waitable() { +- @Override +- protected Object evaluate() { +- ServerGamePacketListenerImpl.this.disconnect(reason, cause); // Paper - adventure, kick event cause +- return null; +- } +- }; +- +- this.server.processQueue.add(waitable); +- +- try { +- waitable.get(); +- } catch (InterruptedException e) { +- Thread.currentThread().interrupt(); +- } catch (ExecutionException e) { +- throw new RuntimeException(e); +- } ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.player)) { // Paper - region threading ++ this.connection.disconnectSafely(PaperAdventure.asVanilla(reason), cause); // Paper - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region + return; + } + +@@ -525,7 +526,6 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + Objects.requireNonNull(this.connection); + // CraftBukkit - Don't wait +- minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper + } + + private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { +@@ -608,9 +608,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + // Paper end - fix large move vectors killing the server + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Paper - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Paper - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Paper - region threading + + ++this.receivedMovePacketCount; + int i = this.receivedMovePacketCount - this.knownMovePacketCount; +@@ -864,13 +865,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async + // CraftBukkit start + if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable +- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause ++ this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - region threading + return; + } + // Paper start + String str = packet.getCommand(); int index = -1; + if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) { +- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause ++ this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - region threading + return; + } + // Paper end +@@ -895,7 +896,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (!event.isHandled()) { + if (!event.isCancelled()) { + +- this.server.scheduleOnMain(() -> { // This needs to be on main ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading + ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); + + this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { +@@ -906,7 +907,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions())); + // Paper end - Brigadier API + }); +- }); ++ }, null, 1L); // Paper - region threading + } + } else if (!completions.isEmpty()) { + final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength()); +@@ -1215,7 +1216,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (byteLength > 256 * 4) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); +- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause ++ this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - region threading + return; + } + byteTotal += byteLength; +@@ -1238,17 +1239,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + if (byteTotal > byteAllowed) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); +- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause ++ this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - region threading + return; + } + } + // Paper end + // CraftBukkit start +- if (this.lastBookTick + 20 > MinecraftServer.currentTick) { +- server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main ++ if (this.lastBookTick + 20 > this.lastTick) { ++ this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - Also ensure this is called on main // Paper - region threading + return; + } +- this.lastBookTick = MinecraftServer.currentTick; ++ this.lastBookTick = this.lastTick; + // CraftBukkit end + int i = packet.getSlot(); + +@@ -1435,9 +1436,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + int i = this.receivedMovePacketCount - this.knownMovePacketCount; + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Paper - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Paper - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Paper - region threading + + if (i > Math.max(this.allowedPlayerTicks, 5)) { + ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); +@@ -1829,9 +1831,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (!this.player.isSpectator()) { + // limit how quickly items can be dropped + // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. +- if (this.lastDropTick != MinecraftServer.currentTick) { ++ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick()) { + this.dropCount = 0; +- this.lastDropTick = MinecraftServer.currentTick; ++ this.lastDropTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); + } else { + // Else we increment the drop count and check the amount. + this.dropCount++; +@@ -2056,7 +2058,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + Entity entity = packet.getEntity(worldserver); + + if (entity != null) { +- this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Paper - region threading + return; + } + } +@@ -2117,6 +2119,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + this.player.disconnect(); + // Paper start - Adventure + quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used ++ this.disconnectPos = this.player.chunkPosition(); // Paper - region threading - note: only set after removing, since it can tick the player ++ this.player.getLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Paper - region threading - force chunk to be loaded so that the region is not lost + if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); + // Paper end +@@ -2201,9 +2205,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + } + // CraftBukkit end + if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) { +- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing ++ // Paper - region threading + this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause +- }); // Paper - push to main for event firing ++ // Paper - region threading + } else { + Optional optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages()); + +@@ -2237,17 +2241,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + @Override + public void handleChatCommand(ServerboundChatCommandPacket packet) { + if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) { +- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing ++ // Paper - region threading + this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper +- }); // Paper - push to main for event firing ++ // Paper - region threading + } else { + Optional optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages()); + + if (optional.isPresent()) { +- this.server.submit(() -> { ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Paper - region threading + this.performChatCommand(packet, (LastSeenMessages) optional.get()); + this.detectRateSpam("/" + packet.command()); // Spigot +- }); ++ }, null, 1L); // Paper - region threading + } + + } +@@ -2321,9 +2325,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + private Optional tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) { + if (!this.updateChatOrder(timestamp)) { + ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper +- this.server.scheduleOnMain(() -> { // Paper - push to main ++ // Paper - region threading + this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca +- }); // Paper - push to main ++ // Paper - region threading + return Optional.empty(); + } else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales + this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false)); +@@ -2396,7 +2400,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + String originalFormat = event.getFormat(), originalMessage = event.getMessage(); + this.cserver.getPluginManager().callEvent(event); + +- if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Paper - region threading + // Evil plugins still listening to deprecated event + final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); + queueEvent.setCancelled(event.isCancelled()); +@@ -2474,6 +2478,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + public void handleCommand(String s) { // Paper - private -> public + // Paper Start + if (!org.spigotmc.AsyncCatcher.shuttingDown && !org.bukkit.Bukkit.isPrimaryThread()) { ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + LOGGER.error("Command Dispatched Async: " + s); + LOGGER.error("Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable()); + Waitable wait = new Waitable<>() { +@@ -2534,6 +2539,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + if (s.isEmpty()) { + ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message"); + } else if (this.getCraftPlayer().isConversing()) { ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + final String conversationInput = s; + this.server.processQueue.add(new Runnable() { + @Override +@@ -2889,6 +2895,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + switch (packetplayinclientcommand_enumclientcommand) { + case PERFORM_RESPAWN: + if (this.player.wonGame) { ++ // Paper start - region threading ++ if (true) { ++ this.player.exitEndCredits(); ++ return; ++ } ++ // Paper end - region threading + this.player.wonGame = false; + this.player = this.server.getPlayerList().respawn(this.player, this.server.getLevel(this.player.getRespawnDimension()), true, null, true, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL); // Paper - add isEndCreditsRespawn argument + CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD); +@@ -2897,6 +2909,18 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + return; + } + ++ // Paper start - region threading ++ if (true) { ++ this.player.respawn((ServerPlayer player) -> { ++ if (ServerGamePacketListenerImpl.this.server.isHardcore()) { ++ player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper ++ ((GameRules.BooleanValue) player.getLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, player.getLevel()); // Paper ++ } ++ }); ++ return; ++ } ++ // Paper end - region threading ++ + this.player = this.server.getPlayerList().respawn(this.player, false); + if (this.server.isHardcore()) { + this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper +@@ -3249,7 +3273,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + // Paper start + if (!org.bukkit.Bukkit.isPrimaryThread()) { + if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) { +- server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause ++ this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - region threading + return; + } + } +@@ -3391,7 +3415,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + + this.filterTextPacket(list).thenAcceptAsync((list1) -> { + this.updateSignText(packet, list1); +- }, this.server); ++ }, (Runnable run) -> { // Paper start - region threading ++ this.player.getBukkitEntity().taskScheduler.schedule( ++ (player) -> { ++ run.run(); ++ }, ++ null, 1L); ++ }); + } + + private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index a25306fe8a35bb70a490e6a0c01d0340bbc0d781..9f06b025e09dfb5b148da536fec33293577e8a81 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -53,7 +53,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + private final byte[] challenge; + final MinecraftServer server; + public final Connection connection; +- public ServerLoginPacketListenerImpl.State state; ++ public volatile ServerLoginPacketListenerImpl.State state; // Paper - region threading + private int tick; + public @Nullable + GameProfile gameProfile; +@@ -80,20 +80,14 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + } + // Paper end + if (this.state == ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT) { +- // Paper start - prevent logins to be processed even though disconnect was called +- if (connection.isConnected()) { ++ // Paper start - region threading - rewrite login process ++ String name = this.gameProfile.getName(); ++ UUID uniqueId = UUIDUtil.getOrCreatePlayerUUID(this.gameProfile); ++ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { + this.handleAcceptedLogin(); + } +- // Paper end +- } else if (this.state == ServerLoginPacketListenerImpl.State.DELAY_ACCEPT) { +- ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId()); +- +- if (entityplayer == null) { +- this.state = ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT; +- this.placeNewPlayer(this.delayedAcceptPlayer); +- this.delayedAcceptPlayer = null; +- } +- } ++ // Paper end - region threading - rewrite login process ++ } // Paper - region threading - remove delayed accept + + if (this.tick++ == 600) { + this.disconnect(Component.translatable("multiplayer.disconnect.slow_login")); +@@ -163,7 +157,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + // this.disconnect(ichatbasecomponent); + // CraftBukkit end + } else { +- this.state = ServerLoginPacketListenerImpl.State.ACCEPTED; ++ this.state = ServerLoginPacketListenerImpl.State.HANDING_OFF; // Paper - region threading - rewrite login process + if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) { + this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> { + this.connection.setupCompression(this.server.getCompressionThreshold(), true); +@@ -171,17 +165,55 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + } + + this.connection.send(new ClientboundGameProfilePacket(this.gameProfile)); +- ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId()); ++ // Paper - region threading - rewrite login process + + try { +- ServerPlayer entityplayer1 = this.server.getPlayerList().getPlayerForLogin(this.gameProfile, s); // CraftBukkit - add player reference +- +- if (entityplayer != null) { +- this.state = ServerLoginPacketListenerImpl.State.DELAY_ACCEPT; +- this.delayedAcceptPlayer = entityplayer1; +- } else { +- this.placeNewPlayer(entityplayer1); +- } ++ // Paper start - region threading - rewrite login process ++ org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ ca.spottedleaf.concurrentutil.completable.Completable toComplete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously ++ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, ++ // on the load callback for those chunks (so on the same region) ++ // this guarantees the chunk cannot unload under our feet ++ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { ++ int chunkX = net.minecraft.util.Mth.fastFloor(loc.getX()) >> 4; ++ int chunkZ = net.minecraft.util.Mth.fastFloor(loc.getZ()) >> 4; ++ ++ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); ++ // we just need to hold the chunks at loaded until the next tick ++ // so we do not need to care about unique IDs for the ticket ++ world.getChunkSource().addTicketAtLevel( ++ net.minecraft.server.level.TicketType.LOGIN, ++ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // now at this point the connection is held by the region, so we have to check isConnected() ++ // this would have been handled in connection ticking, but we are in a state between ++ // being owned by the global tick thread and the region so we have to do it ++ if (t != null || !ServerLoginPacketListenerImpl.this.connection.isConnected()) { ++ ServerLoginPacketListenerImpl.this.connection.handleDisconnection(); ++ return; ++ } ++ ServerLoginPacketListenerImpl.this.state = State.ACCEPTED; ++ ServerLoginPacketListenerImpl.this.server.getPlayerList().placeNewPlayer( ++ ServerLoginPacketListenerImpl.this.connection, ++ s, ++ data.getValue(), ++ lastKnownName.getValue(), ++ loc ++ ); ++ }, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER ++ ); ++ }); ++ this.server.getPlayerList().loadSpawnForNewPlayer(this.connection, s, data, lastKnownName, toComplete); ++ // Paper end - region threading - rewrite login process + } catch (Exception exception) { + ServerLoginPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception); + MutableComponent ichatmutablecomponent = Component.translatable("multiplayer.disconnect.invalid_player_data"); +@@ -198,9 +230,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + + } + +- private void placeNewPlayer(ServerPlayer player) { +- this.server.getPlayerList().placeNewPlayer(this.connection, player); +- } ++ // Paper end - region threading - rewrite login process + + @Override + public void onDisconnect(Component reason) { +@@ -397,7 +427,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + uniqueId = gameProfile.getId(); + // Paper end + +- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Paper - region threading + final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); + if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { + event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure +@@ -480,7 +510,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + + public static enum State { + +- HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, ACCEPTED; ++ HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, HANDING_OFF, ACCEPTED; // Paper - region threading + + private State() {} + } +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 3c9d08c37a44a60bc70387d8d0dbd0a39ea98a26..56603d8d817657ef5de23ccfd58a921be5f58e8c 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -138,7 +138,7 @@ public abstract class PlayerList { + private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private final MinecraftServer server; + public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety +- private final Map playersByUUID = Maps.newHashMap(); ++ private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! + private final UserBanList bans; + private final IpBanList ipBans; + private final ServerOpList ops; +@@ -160,9 +160,56 @@ public abstract class PlayerList { + + // CraftBukkit start + private CraftServer cserver; +- private final Map playersByName = new java.util.HashMap<>(); ++ private final Map playersByName = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! + public @Nullable String collideRuleTeamName; // Paper - Team name used for collideRule + ++ // Paper start - region threading ++ private final Object stateLock = new Object(); ++ private final Map connectionByName = new java.util.HashMap<>(); ++ private final Map connectionById = new java.util.HashMap<>(); ++ ++ public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ Connection conflictingName, conflictingId; ++ synchronized (this.stateLock) { ++ conflictingName = this.connectionByName.get(userName); ++ conflictingId = this.connectionById.get(byId); ++ ++ if (conflictingName == null && conflictingId == null) { ++ this.connectionByName.put(userName, conn); ++ this.connectionById.put(byId, conn); ++ } ++ } ++ ++ Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); ++ ++ if (conflictingId != null || conflictingName != null) { ++ if (conflictingName != null && conflictingName.isPlayerConnected()) { ++ conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { ++ conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ } ++ ++ return conflictingName == null && conflictingId == null; ++ } ++ ++ public void removeConnection(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ synchronized (this.stateLock) { ++ this.connectionByName.remove(userName, conn); ++ this.connectionById.remove(byId, conn); ++ } ++ } ++ ++ private int getTotalConnections() { ++ synchronized (this.stateLock) { ++ return this.connectionById.size(); ++ } ++ } ++ // Paper end - region threading ++ + public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { + this.cserver = server.server = new CraftServer((DedicatedServer) server, this); + server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper +@@ -184,7 +231,7 @@ public abstract class PlayerList { + } + abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor + +- public void placeNewPlayer(Connection connection, ServerPlayer player) { ++ public void loadSpawnForNewPlayer(Connection connection, ServerPlayer player, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper + GameProfile gameprofile = player.getGameProfile(); +@@ -234,8 +281,28 @@ public abstract class PlayerList { + worldserver1 = worldserver; + } + +- if (nbttagcompound == null) player.fudgeSpawnLocation(worldserver1); // Paper - only move to spawn on first login, otherwise, stay where you are.... +- ++ // Paper start - region threading - rewrite login process ++ if (nbttagcompound == null) ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // Paper - only move to spawn on first login, otherwise, stay where you are.... ++ if (nbttagcompound != null) { ++ worldserver1.loadChunksForMoveAsync( ++ player.getBoundingBox(), ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (c) -> { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position())); ++ } ++ ); ++ } ++ data.setValue(nbttagcompound); ++ lastKnownName.setValue(s); ++ return; ++ } ++ // nbttagcomound -> player data ++ // s -> last known name ++ public void placeNewPlayer(Connection connection, ServerPlayer player, CompoundTag nbttagcompound, String s, Location selectedSpawn) { ++ ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle(); ++ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); ++ player.lastSave = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); ++ // Paper end - region threading - rewrite login process + player.setLevel(worldserver1); + String s1 = "local"; + +@@ -246,7 +313,7 @@ public abstract class PlayerList { + // Spigot start - spawn location event + Player spawnPlayer = player.getBukkitEntity(); + org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new com.destroystokyo.paper.event.player.PlayerInitialSpawnEvent(spawnPlayer, spawnPlayer.getLocation()); // Paper use our duplicate event +- this.cserver.getPluginManager().callEvent(ev); ++ //this.cserver.getPluginManager().callEvent(ev); // Paper - region threading - TODO WTF TO DO WITH THIS EVENT? + + Location loc = ev.getSpawnLocation(); + worldserver1 = ((CraftWorld) loc.getWorld()).getHandle(); +@@ -265,6 +332,7 @@ public abstract class PlayerList { + + player.loadGameTypes(nbttagcompound); + ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player); ++ worldserver1.getCurrentWorldData().connections.add(connection); // Paper - region threading - only AFTER updating listener to game + GameRules gamerules = worldserver1.getGameRules(); + boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); + boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); +@@ -282,7 +350,7 @@ public abstract class PlayerList { + this.sendPlayerPermissionLevel(player); + player.getStats().markAllDirty(); + player.getRecipeBook().sendInitialRecipeBook(player); +- this.updateEntireScoreboard(worldserver1.getScoreboard(), player); ++ if (false) this.updateEntireScoreboard(worldserver1.getScoreboard(), player); // Paper - region threading + this.server.invalidateStatus(); + MutableComponent ichatmutablecomponent; + +@@ -334,8 +402,7 @@ public abstract class PlayerList { + ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); + + final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - use single player info update packet +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer1 : this.players) { // Paper - region threadingv + + if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { + entityplayer1.connection.send(packet); +@@ -451,7 +518,7 @@ public abstract class PlayerList { + // Paper start - Add to collideRule team if needed + final Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); +- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { ++ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Paper - region threading + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end +@@ -542,7 +609,7 @@ public abstract class PlayerList { + + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit +- player.lastSave = MinecraftServer.currentTick; // Paper ++ player.lastSave = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - region threading + this.playerIo.save(player); + ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit + +@@ -582,7 +649,7 @@ public abstract class PlayerList { + // CraftBukkit end + + // Paper start - Remove from collideRule team if needed +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Paper - region threading + final Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); + if (entityplayer.getTeam() == team && team != null) { +@@ -622,6 +689,7 @@ public abstract class PlayerList { + + entityplayer.unRide(); + worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); ++ entityplayer.retireScheduler(); // Paper - region threading + entityplayer.getAdvancements().stopListening(); + this.players.remove(entityplayer); + this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -640,8 +708,7 @@ public abstract class PlayerList { + // CraftBukkit start + // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()))); + ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())); +- for (int i = 0; i < this.players.size(); i++) { +- ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer2 : this.players) { // Paper - region threading + + if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) { + entityplayer2.connection.send(packet); +@@ -666,19 +733,13 @@ public abstract class PlayerList { + + ServerPlayer entityplayer; + +- for (int i = 0; i < this.players.size(); ++i) { +- entityplayer = (ServerPlayer) this.players.get(i); +- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames +- list.add(entityplayer); +- } +- } ++ // Paper - region threading - rewrite login process - moved to pushPendingJoin + + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { + entityplayer = (ServerPlayer) iterator.next(); +- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved +- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause ++ // Paper - moved to pushPendingJoin + } + + // Instead of kicking then returning, we need to store the kick reason +@@ -717,7 +778,7 @@ public abstract class PlayerList { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure + } else { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { ++ if (this.getTotalConnections() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { // Paper - region threading - we control connection state here now async, not player list size + event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -967,10 +1028,10 @@ public abstract class PlayerList { + public void tick() { + if (++this.sendAllPlayerInfoIn > 600) { + // CraftBukkit start +- for (int i = 0; i < this.players.size(); ++i) { +- final ServerPlayer target = (ServerPlayer) this.players.get(i); ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Paper - region threading ++ for (final ServerPlayer target : players) { // Paper - region threading + +- target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { ++ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), java.util.Arrays.stream(players).filter(new Predicate() { // Paper - region threading + @Override + public boolean test(ServerPlayer input) { + return target.getBukkitEntity().canSee(input.getBukkitEntity()); +@@ -996,18 +1057,17 @@ public abstract class PlayerList { + + // CraftBukkit start - add a world/entity limited version + public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Paper - region threading + if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { + continue; + } +- ((ServerPlayer) this.players.get(i)).connection.send(packet); ++ entityplayer.connection.send(packet); // Paper - region threading + } + } + + public void broadcastAll(Packet packet, Level world) { +- for (int i = 0; i < world.players().size(); ++i) { +- ((ServerPlayer) world.players().get(i)).connection.send(packet); ++ for (net.minecraft.world.entity.player.Player player : world.players()) { // Paper - region threading ++ ((ServerPlayer) player).connection.send(packet); // Paper - region threading + } + + } +@@ -1051,8 +1111,7 @@ public abstract class PlayerList { + if (scoreboardteambase == null) { + this.broadcastSystemMessage(message, false); + } else { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Paper - region threading + + if (entityplayer.getTeam() != scoreboardteambase) { + entityplayer.sendSystemMessage(message); +@@ -1063,10 +1122,12 @@ public abstract class PlayerList { + } + + public String[] getPlayerNamesArray() { +- String[] astring = new String[this.players.size()]; ++ List players = new java.util.ArrayList<>(this.players); // Paper start - region threading ++ String[] astring = new String[players.size()]; + +- for (int i = 0; i < this.players.size(); ++i) { +- astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName(); ++ for (int i = 0; i < players.size(); ++i) { ++ astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName(); ++ // Paper end - region threading + } + + return astring; +@@ -1085,7 +1146,9 @@ public abstract class PlayerList { + ServerPlayer entityplayer = this.getPlayer(profile.getId()); + + if (entityplayer != null) { ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading + this.sendPlayerPermissionLevel(entityplayer); ++ }, null, 1L); // Paper - region threading + } + + } +@@ -1095,7 +1158,10 @@ public abstract class PlayerList { + ServerPlayer entityplayer = this.getPlayer(profile.getId()); + + if (entityplayer != null) { ++ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Paper - region threading + this.sendPlayerPermissionLevel(entityplayer); ++ }, null, 1L); // Paper - region threading ++ + } + + } +@@ -1156,8 +1222,7 @@ public abstract class PlayerList { + } + + public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Paper - region threading + + // CraftBukkit start - Test if player receiving packet can see the source of the packet + if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) { +@@ -1187,9 +1252,12 @@ public abstract class PlayerList { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + MinecraftTimings.savePlayers.startTiming(); // Paper + int numSaved = 0; +- long now = MinecraftServer.currentTick; +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = this.players.get(i); ++ long now = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - region threading ++ for (ServerPlayer entityplayer : this.players) { // Paper start - region threading ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entityplayer)) { ++ continue; ++ } ++ // Paper end - region threading + if (interval == -1 || now - entityplayer.lastSave >= interval) { + this.save(entityplayer); + if (interval != -1 && ++numSaved <= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } +@@ -1309,6 +1377,20 @@ public abstract class PlayerList { + } + + public void removeAll(boolean isRestarting) { ++ // Paper start - region threading ++ // just send disconnect packet, don't modify state ++ for (ServerPlayer player : this.players) { ++ final Component ichatbasecomponent = PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure ++ // CraftBukkit end ++ ++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(ichatbasecomponent), net.minecraft.network.PacketSendListener.thenRun(() -> { ++ player.connection.disconnect(ichatbasecomponent); ++ })); ++ } ++ if (true) { ++ return; ++ } ++ // Paper end - region threading + // Paper end + // CraftBukkit start - disconnect safely + for (ServerPlayer player : this.players) { +@@ -1318,7 +1400,7 @@ public abstract class PlayerList { + // CraftBukkit end + + // Paper start - Remove collideRule team if it exists +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Paper - region threading + final Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); + if (team != null) scoreboard.removePlayerTeam(team); +diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java +index 4fd709a550bf8da1e996894a1ca6b91206c31e9e..8b96df8a6a96b976c1bad4f899d3332b1a4ec7a9 100644 +--- a/src/main/java/net/minecraft/server/players/StoredUserList.java ++++ b/src/main/java/net/minecraft/server/players/StoredUserList.java +@@ -148,6 +148,7 @@ public abstract class StoredUserList> { + } + + public void save() throws IOException { ++ synchronized (this) { // Paper - region threading + this.removeExpired(); // Paper - remove expired values before saving + JsonArray jsonarray = new JsonArray(); + Stream stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error +@@ -178,10 +179,12 @@ public abstract class StoredUserList> { + if (bufferedwriter != null) { + bufferedwriter.close(); + } ++ } // Paper - region threading + + } + + public void load() throws IOException { ++ synchronized (this) { // Paper - region threading + if (this.file.exists()) { + BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8); + +@@ -226,5 +229,6 @@ public abstract class StoredUserList> { + } + + } ++ } // Paper - region threading + } + } +diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java +index 4f5f2c25e12ee6d977bc98d9118650cfe91e6c0e..4f32db746ffd4d4f8e2bdecc91a46824ca05aca0 100644 +--- a/src/main/java/net/minecraft/util/SortedArraySet.java ++++ b/src/main/java/net/minecraft/util/SortedArraySet.java +@@ -82,7 +82,7 @@ public class SortedArraySet extends AbstractSet { + return Arrays.binarySearch(this.contents, 0, this.size, object, this.comparator); + } + +- private static int getInsertionPosition(int binarySearchResult) { ++ public static int getInsertionPosition(int binarySearchResult) { // Paper - region threading - public + return -binarySearchResult - 1; + } + +@@ -169,6 +169,40 @@ public class SortedArraySet extends AbstractSet { + } + } + // Paper end - rewrite chunk system ++ // Paper start - region threading ++ public int binarySearch(final T search) { ++ return this.findIndex(search); ++ } ++ ++ public int insertAndGetIdx(final T value) { ++ final int idx = this.findIndex(value); ++ if (idx >= 0) { ++ // exists already ++ return idx; ++ } ++ ++ this.addInternal(value, getInsertionPosition(idx)); ++ return idx; ++ } ++ ++ public T removeFirst() { ++ final T ret = this.contents[0]; ++ ++ this.removeInternal(0); ++ ++ return ret; ++ } ++ ++ public T removeLast() { ++ final int index = --this.size; ++ ++ final T ret = this.contents[index]; ++ ++ this.contents[index] = null; ++ ++ return ret; ++ } ++ // Paper end - region threading + + @Override + public boolean remove(Object object) { +diff --git a/src/main/java/net/minecraft/util/SpawnUtil.java b/src/main/java/net/minecraft/util/SpawnUtil.java +index 83ef8cb27db685cceb5c2b7c9674e17b93ba081c..720b7a43356becbdf82f700e17ca0de7c717fc8d 100644 +--- a/src/main/java/net/minecraft/util/SpawnUtil.java ++++ b/src/main/java/net/minecraft/util/SpawnUtil.java +@@ -59,7 +59,7 @@ public class SpawnUtil { + return Optional.of(t0); + } + +- t0.discard(); ++ //t0.discard(); // Paper - region threading + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 1eaab1f6923e6aa34b643293347348e5cc19af3c..53b6c6c850625176e2f4632d38e5fed70b532d46 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -165,7 +165,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + // Paper start + public static RandomSource SHARED_RANDOM = new RandomRandomSource(); +- private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { ++ public static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { // Paper - region threading + private boolean locked = false; + + @Override +@@ -239,17 +239,29 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper + public boolean collisionLoadChunks = false; // Paper +- private CraftEntity bukkitEntity; ++ private volatile CraftEntity bukkitEntity; // Paper - region threading + + public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper + public @Nullable Throwable addedToWorldStack; // Paper - entity debug + public CraftEntity getBukkitEntity() { + if (this.bukkitEntity == null) { +- this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); ++ // Paper start - region threading ++ synchronized (this) { ++ if (this.bukkitEntity == null) { ++ return this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); ++ } ++ } ++ // Paper end - region threading + } + return this.bukkitEntity; + } + ++ // Paper start - region threading ++ public CraftEntity getBukkitEntityRaw() { ++ return this.bukkitEntity; ++ } ++ // Paper end - region threading ++ + @Override + public CommandSender getBukkitSender(CommandSourceStack wrapper) { + return this.getBukkitEntity(); +@@ -488,28 +500,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.isLegacyTrackingEntity = isLegacyTrackingEntity; + } + +- public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { +- // determine highest range of passengers +- if (this.passengers.isEmpty()) { +- return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] +- .getObjectsInRange(MCUtil.getCoordinateKey(this)); +- } +- Iterable passengers = this.getIndirectPassengers(); +- net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; +- org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; +- int range = chunkMap.getEntityTrackerRange(type.ordinal()); +- +- for (Entity passenger : passengers) { +- org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; +- int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); +- if (passengerRange > range) { +- type = passengerType; +- range = passengerRange; +- } +- } +- +- return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); +- } ++ // Paper - region threading + // Paper end - optimise entity tracking + // Paper start - make end portalling safe + public BlockPos portalBlock; +@@ -541,6 +532,25 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.teleportTo(worldserver, null); + } + // Paper end - make end portalling safe ++ // Paper start ++ private static final java.util.concurrent.ConcurrentHashMap, Integer> CLASS_ID_MAP = new java.util.concurrent.ConcurrentHashMap<>(); ++ private static final AtomicInteger CLASS_ID_GENERATOR = new AtomicInteger(); ++ public final int classId = CLASS_ID_MAP.computeIfAbsent(this.getClass(), (Class c) -> { ++ return CLASS_ID_GENERATOR.getAndIncrement(); ++ }); ++ private static final java.util.concurrent.atomic.AtomicLong REFERENCE_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); ++ public final long referenceId = REFERENCE_ID_GENERATOR.getAndIncrement(); ++ // Paper end ++ // Paper start - region ticking ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ if (this.activatedTick != Integer.MIN_VALUE) { ++ this.activatedTick += fromTickOffset; ++ } ++ if (this.activatedImmunityTick != Integer.MIN_VALUE) { ++ this.activatedImmunityTick += fromTickOffset; ++ } ++ } ++ // Paper end - region ticking + + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); +@@ -656,6 +666,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public final void discard() { ++ // Paper start - region threading ++ if (this.isRemoved()) { ++ return; ++ } ++ // Paper end - region threading + this.remove(Entity.RemovalReason.DISCARDED); + } + +@@ -780,6 +795,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + // CraftBukkit start + public void postTick() { ++ // Paper start - region threading ++ // moved to doPortalLogic ++ if (true) { ++ return; ++ } ++ // Paper end - region threading + // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle + if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities + this.handleNetherPortal(); +@@ -802,7 +823,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.walkDistO = this.walkDist; + this.xRotO = this.getXRot(); + this.yRotO = this.getYRot(); +- if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick ++ //if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick // Paper - region threading - ONLY allow in postTick() + if (this.canSpawnSprintParticle()) { + this.spawnSprintParticle(); + } +@@ -903,11 +924,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // This will be called every single tick the entity is in lava, so don't throw an event + this.setSecondsOnFire(15, false); + } +- CraftEventFactory.blockDamage = (this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact); ++ CraftEventFactory.blockDamageRT.set((this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact)); // Paper - region threading + if (this.hurt(DamageSource.LAVA, 4.0F)) { + this.playSound(SoundEvents.GENERIC_BURN, 0.4F, 2.0F + this.random.nextFloat() * 0.4F); + } +- CraftEventFactory.blockDamage = null; ++ CraftEventFactory.blockDamageRT.set(null); // Paper - region threading + // CraftBukkit end - we also don't throw an event unless the object in lava is living, to save on some event calls + + } +@@ -1015,8 +1036,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } else { + this.wasOnFire = this.isOnFire(); + if (movementType == MoverType.PISTON) { +- this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper +- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper ++ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 20); // Paper ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 20); // Paper + movement = this.limitPistonMovement(movement); + if (movement.equals(Vec3.ZERO)) { + return; +@@ -3071,6 +3092,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + @Nullable + public Team getTeam() { ++ // Paper start - region threading ++ if (true) { ++ return null; ++ } ++ // Paper end - region threading + if (!this.level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper + return this.level.getScoreboard().getPlayersTeam(this.getScoreboardName()); + } +@@ -3186,9 +3212,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + if (this.fireImmune()) { + return; + } +- CraftEventFactory.entityDamage = lightning; ++ CraftEventFactory.entityDamageRT.set(lightning); // Paper - region threading + if (!this.hurt(DamageSource.LIGHTNING_BOLT, 5.0F)) { +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // Paper - region threading + return; + } + // CraftBukkit end +@@ -3361,6 +3387,628 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.portalEntrancePos = original.portalEntrancePos; + } + ++ // Paper start - region threading ++ public static class EntityTreeNode { ++ @Nullable ++ public EntityTreeNode parent; ++ public Entity root; ++ @Nullable ++ public EntityTreeNode[] passengers; ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root) { ++ this.parent = parent; ++ this.root = root; ++ } ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { ++ this.parent = parent; ++ this.root = root; ++ this.passengers = passengers; ++ } ++ ++ public List getFullTree() { ++ List ret = new java.util.ArrayList<>(); ++ ret.add(this); ++ ++ // this is just a BFS except we don't remove from head, we just advance down the list ++ for (int i = 0; i < ret.size(); ++i) { ++ EntityTreeNode node = ret.get(i); ++ ++ EntityTreeNode[] passengers = node.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ for (EntityTreeNode passenger : passengers) { ++ ret.add(passenger); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void restore() { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ List newPassengers = new java.util.ArrayList<>(); ++ for (EntityTreeNode passenger : passengers) { ++ newPassengers.add(passenger.root); ++ passenger.root.vehicle = curr.root; ++ } ++ ++ curr.root.passengers = ImmutableList.copyOf(newPassengers); ++ } ++ } ++ ++ public void adjustRiders() { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ for (EntityTreeNode passenger : passengers) { ++ curr.root.positionRider(passenger.root, Entity::moveTo); ++ } ++ } ++ } ++ } ++ ++ protected EntityTreeNode makePassengerTree() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ protected EntityTreeNode detachPassengers() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ vehicle.passengers = ImmutableList.of(); ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ passenger.vehicle = null; ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ /** ++ * This flag will perform an async load on the chunks determined by ++ * the entity's bounding box before teleporting the entity. ++ */ ++ public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; ++ /** ++ * This flag requires the entity being teleported to be a root vehicle. ++ * Thus, if you want to teleport a non-root vehicle, you must dismount ++ * the target entity before calling teleport, otherwise the ++ * teleport will be refused. ++ */ ++ public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; ++ ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ destination.addDuringTeleport(this); ++ } ++ ++ protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, ++ EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { ++ Vec3 pos = this.position(); ++ ++ Runnable scheduleEntityJoin = () -> { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ destination, ++ io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos), ++ () -> { ++ List fullTree = passengerTree.getFullTree(); ++ for (EntityTreeNode node : fullTree) { ++ node.root.placeSingleSync(originWorld, destination, node, teleportFlags); ++ } ++ ++ // restore passenger tree ++ passengerTree.restore(); ++ passengerTree.adjustRiders(); ++ ++ // invoke post dimension change now ++ for (EntityTreeNode node : fullTree) { ++ node.root.postChangeDimension(); ++ } ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(Entity.this); ++ } ++ } ++ ); ++ }; ++ ++ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { ++ destination.loadChunksForMoveAsync( ++ Entity.this.getBoundingBox(), ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ (chunkList) -> { ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { ++ destination.chunkSource.addTicketAtLevel( ++ TicketType.POST_TELEPORT, chunk.getPos(), ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ Integer.valueOf(Entity.this.getId()) ++ ); ++ } ++ scheduleEntityJoin.run(); ++ } ++ ); ++ } else { ++ scheduleEntityJoin.run(); ++ } ++ } ++ ++ protected boolean canTeleportAsync() { ++ return !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); ++ } ++ ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (speedDirectionUpdate != null) { ++ this.setDeltaMovement(speedDirectionUpdate.normalize().scale(this.getDeltaMovement().length())); ++ } ++ this.teleportTo(pos.x, pos.y, pos.z); ++ } ++ ++ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (speedDirectionUpdate != null) { ++ this.setDeltaMovement(speedDirectionUpdate); ++ } ++ if (pos != null) { ++ this.setPosRaw(pos.x, pos.y, pos.z); ++ } ++ } ++ ++ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { ++ Entity copy = this.getType().create(destination); ++ copy.restoreFrom(this); ++ copy.transform(pos, yaw, pitch, speedDirectionUpdate); ++ ++ this.removeAfterChangingDimensions(); ++ ++ return copy; ++ } ++ ++ public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, ++ java.util.function.Consumer teleportComplete) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); ++ ++ if (!ServerLevel.isInSpawnableBounds(new BlockPos(pos))) { ++ return false; ++ } ++ ++ if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { ++ if (this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || this.isPassenger()) { ++ return false; ++ } ++ } ++ ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if (!this.canTeleportAsync()) { ++ return false; ++ } ++ for (Entity entity : this.getIndirectPassengers()) { ++ entity.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if (!entity.canTeleportAsync()) { ++ return false; ++ } ++ } ++ ++ // TODO any events that can modify go HERE ++ ++ // check for same region ++ if (destination == this.getLevel()) { ++ Vec3 currPos = this.position(); ++ if ( ++ destination.regioniser.getRegionAtUnsynchronised( ++ io.papermc.paper.util.CoordinateUtils.getChunkX(currPos), io.papermc.paper.util.CoordinateUtils.getChunkZ(currPos) ++ ) == destination.regioniser.getRegionAtUnsynchronised( ++ io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos) ++ ) ++ ) { ++ EntityTreeNode passengerTree = this.makePassengerTree(); ++ for (EntityTreeNode entity : passengerTree.getFullTree()) { ++ entity.root.teleportSyncSameRegion(pos, yaw, pitch, speedDirectionUpdate); ++ } ++ ++ passengerTree.adjustRiders(); ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(this); ++ } ++ return true; ++ } ++ } ++ ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, speedDirectionUpdate); ++ } ++ ++ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); ++ ++ return true; ++ } ++ ++ public void preChangeDimension() { ++ ++ } ++ ++ public void postChangeDimension() { ++ ++ } ++ ++ protected static enum PortalType { ++ NETHER, END; ++ } ++ ++ public boolean doPortalLogic() { ++ if (this.tryNetherPortal()) { ++ return true; ++ } ++ if (this.tryEndPortal()) { ++ return true; ++ } ++ return false; ++ } ++ ++ protected boolean tryEndPortal() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ BlockPos pos = this.portalBlock; ++ ServerLevel world = this.portalWorld; ++ this.portalBlock = null; ++ this.portalWorld = null; ++ ++ if (pos == null || world == null || world != this.level) { ++ return false; ++ } ++ ++ if (this.isPassenger() || this.isVehicle() || !this.canChangeDimensions() || this.isRemoved() || !this.valid || !this.isAlive()) { ++ return false; ++ } ++ ++ return this.endPortalLogicAsync(); ++ } ++ ++ protected boolean tryNetherPortal() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ int portalWaitTime = this.getPortalWaitTime(); ++ ++ if (this.isInsidePortal) { ++ // if we are in a nether portal still, this flag will be set next tick. ++ this.isInsidePortal = false; ++ if (this.portalTime++ >= portalWaitTime) { ++ this.portalTime = portalWaitTime; ++ if (this.netherPortalLogicAsync()) { ++ return true; ++ } ++ } ++ } else { ++ // rapidly decrease portal time ++ this.portalTime = Math.max(0, this.portalTime - 4); ++ } ++ ++ this.processPortalCooldown(); ++ return false; ++ } ++ ++ public boolean endPortalLogicAsync() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, false, PortalType.END, null); ++ } ++ ++ public boolean netherPortalLogicAsync() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, false, PortalType.NETHER, null); ++ } ++ ++ // To simplify portal logic, in region threading both players ++ // and non-player entities will create portals. By guaranteeing ++ // that the teleportation can take place, we can simply ++ // remove the entity, find/create the portal, and place async. ++ // If we have to worry about whether the entity may not teleport, ++ // we need to first search, then report back, ... ++ protected void findOrCreatePortalAsync(ServerLevel origin, ServerLevel destination, PortalType type, ++ ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable) { ++ switch (type) { ++ // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld ++ // we just select the spawn position ++ case END: { ++ if (destination.getTypeKey() == LevelStem.END) { ++ BlockPos targetPos = ServerLevel.END_SPAWN_POINT; ++ // need to load chunks so we can create the platform ++ destination.loadChunksAsync( ++ targetPos, 16, // load 16 blocks to be safe from block physics ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, ++ (chunks) -> { ++ ServerLevel.makeObsidianPlatform(destination, null, targetPos); ++ ++ // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity ++ // on the obsidian, we need to spawn at targetPos.y - 1 ++ portalInfoCompletable.complete( ++ new PortalInfo(Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, destination, null) ++ ); ++ } ++ ); ++ } else { ++ BlockPos spawnPos = destination.getSharedSpawnPos(); ++ // need to load chunk for heightmap ++ destination.loadChunksAsync( ++ spawnPos, 0, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, ++ (chunks) -> { ++ BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); ++ ++ // done ++ portalInfoCompletable.complete( ++ new PortalInfo(Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, destination, null) ++ ); ++ } ++ ); ++ } ++ ++ break; ++ } ++ // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) ++ // then we can search for an existing portal using the POI routines ++ // if we don't find a portal, then we bring the chunks in the create radius to full and ++ // create it ++ case NETHER: { ++ // hoisted from the create fallback, so that we can avoid the sync load later if we need it ++ BlockState originalPortalBlock = this.portalEntrancePos == null ? null : origin.getBlockStateIfLoaded(this.portalEntrancePos); ++ Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : ++ originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); ++ BlockUtil.FoundRectangle originalPortalRectangle = ++ originalPortalBlock == null || !originalPortalBlock.hasProperty(BlockStateProperties.HORIZONTAL_AXIS) ++ ? null ++ : BlockUtil.getLargestRectangleAround( ++ this.portalEntrancePos, originalPortalDirection, 21, Direction.Axis.Y, 21, ++ (blockpos) -> { ++ return this.level.getBlockStateIfLoaded(blockpos) == originalPortalBlock; ++ } ++ ); ++ ++ boolean destinationIsNether = destination.getTypeKey() == LevelStem.NETHER; ++ ++ int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? ++ (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : ++ destination.paperConfig().environment.portalSearchRadius; ++ int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; ++ ++ WorldBorder destinationBorder = destination.getWorldBorder(); ++ double dimensionScale = DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); ++ BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); ++ ++ ca.spottedleaf.concurrentutil.completable.Completable portalFound ++ = new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ ++ // post portal find/create logic ++ portalFound.addWaiter( ++ (BlockUtil.FoundRectangle portal, Throwable thr) -> { ++ // no portal could be created ++ if (portal == null) { ++ portalInfoCompletable.complete( ++ new PortalInfo(Vec3.atCenterOf(targetPos), Vec3.ZERO, 90.0f, 0.0f, destination, null) ++ ); ++ return; ++ } ++ ++ Vec3 relativePos = originalPortalRectangle == null ? ++ new Vec3(0.5, 0.0, 0.0) : ++ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); ++ ++ portalInfoCompletable.complete( ++ PortalShape.createPortalInfo( ++ destination, portal, originalPortalDirection, relativePos, ++ Entity.this, Entity.this.getDeltaMovement(), Entity.this.getYRot(), Entity.this.getXRot(), null ++ ) ++ ); ++ } ++ ); ++ ++ // kick off search for existing portal or creation ++ destination.loadChunksAsync( ++ // add 32 so that the final search for a portal frame doesn't load any chunks ++ targetPos, portalSearchRadius + 32, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, ++ (chunks) -> { ++ BlockUtil.FoundRectangle portal = ++ destination.getPortalForcer().findPortalAround(targetPos, destinationBorder, portalSearchRadius).orElse(null); ++ if (portal != null) { ++ portalFound.complete(portal); ++ return; ++ } ++ ++ // no portal found - create one ++ destination.loadChunksAsync( ++ targetPos, portalCreateRadius + 32, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, ++ (chunks2) -> { ++ // we do not have the correct entity reference here ++ BlockUtil.FoundRectangle createdPortal = ++ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); ++ // if it wasn't created, passing null is expected here ++ portalFound.complete(createdPortal); ++ } ++ ); ++ } ++ ); ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown portal type " + type); ++ } ++ } ++ } ++ ++ public boolean canPortalAsync(boolean considerPassengers) { ++ return this.canPortalAsync(considerPassengers, false); ++ } ++ ++ protected boolean canPortalAsync(boolean considerPassengers, boolean skipPassengerCheck) { ++ if (considerPassengers) { ++ if (!skipPassengerCheck && this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { ++ return false; ++ } ++ } ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if (!this.canTeleportAsync() || !this.canChangeDimensions() || this.isOnPortalCooldown()) { ++ return false; ++ } ++ if (considerPassengers) { ++ for (Entity entity : this.passengers) { ++ if (!entity.canPortalAsync(true, true)) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ ++ } ++ ++ protected boolean portalToAsync(ServerLevel destination, boolean takePassengers, ++ PortalType type, java.util.function.Consumer teleportComplete) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ if (!this.canPortalAsync(takePassengers)) { ++ return false; ++ } ++ ++ // first, remove entity/passengers from world ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ node.root.prePortalLogic(originWorld, destination, type); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ // we will update pos/rot/speed later ++ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); ++ // set portal cooldown ++ node.root.setPortalCooldown(); ++ } ++ ++ ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable ++ = new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ ++ portalInfoCompletable.addWaiter((PortalInfo info, Throwable throwable) -> { ++ // adjust passenger tree to final pos ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.transform(info.pos, Float.valueOf(info.yRot), Float.valueOf(info.xRot), info.speed); ++ } ++ ++ // place ++ passengerTree.root.placeInAsync( ++ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), ++ passengerTree, teleportComplete ++ ); ++ }); ++ ++ ++ passengerTree.root.findOrCreatePortalAsync(originWorld, destination, type, portalInfoCompletable); ++ ++ return true; ++ } ++ // Paper end - region threading ++ + @Nullable + public Entity changeDimension(ServerLevel destination) { + // CraftBukkit start +@@ -3859,17 +4507,13 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + // Paper start + public void startSeenByPlayer(ServerPlayer player) { +- if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) { +- new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.getBukkitEntity()).callEvent(); +- } ++ // Paper - region threading - no + } + // Paper end + + // Paper start + public void stopSeenByPlayer(ServerPlayer player) { +- if(io.papermc.paper.event.player.PlayerUntrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) { +- new io.papermc.paper.event.player.PlayerUntrackEntityEvent(player.getBukkitEntity(), this.getBukkitEntity()).callEvent(); +- } ++ // Paper - region threading - no + } + // Paper end + +@@ -4341,7 +4985,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + } + // Paper end - fix MC-4 +- if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; // Paper - region threading ++ if (posChanged) { // Paper - region threading + synchronized (this.posLock) { // Paper + this.position = new Vec3(x, y, z); + } // Paper +@@ -4362,7 +5007,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + // Paper start - never allow AABB to become desynced from position + // hanging has its own special logic +- if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { ++ if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end +@@ -4461,7 +5106,23 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + if (reason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) + this.levelCallback.onRemove(reason); ++ // Paper start - region threading ++ if (!(this instanceof ServerPlayer) && reason != RemovalReason.CHANGED_DIMENSION) { ++ // Players need to be special cased, because they are regularly removed from the world ++ this.retireScheduler(); ++ } ++ // Paper end - region threading ++ } ++ ++ // Paper start - region threading ++ /** ++ * Invoked only when the entity is truly removed from the server, never to be added to any world. ++ */ ++ public final void retireScheduler() { ++ // we need to force create the bukkit entity so that the scheduler can be retired... ++ this.getBukkitEntity().taskScheduler.retire(); + } ++ // Paper end - region threading + + public void unsetRemoved() { + this.removalReason = null; +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 42eb78830855d7282b7f3f1bdbe85e632d489784..3ca2b0c5f2d78d8cdfba76bf7f6020c808b6691b 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -469,7 +469,7 @@ public abstract class LivingEntity extends Entity { + + if (this.isDeadOrDying() && this.level.shouldTickDeath(this)) { + this.tickDeath(); +- } ++ } else { this.broadcastedDeath = false; } // Paper - region threading + + if (this.lastHurtByPlayerTime > 0) { + --this.lastHurtByPlayerTime; +@@ -620,11 +620,13 @@ public abstract class LivingEntity extends Entity { + return false; + } + ++ public boolean broadcastedDeath = false; // Paper - region threading + protected void tickDeath() { + ++this.deathTime; +- if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved()) { ++ if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved() && !this.broadcastedDeath) { // Paper - region threading ++ this.broadcastedDeath = true; // Paper - region threading + this.level.broadcastEntityEvent(this, (byte) 60); +- this.remove(Entity.RemovalReason.KILLED); ++ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED); // Paper - region threading - don't remove, we want the tick scheduler to be running + } + + } +@@ -841,9 +843,9 @@ public abstract class LivingEntity extends Entity { + } + + this.hurtTime = nbt.getShort("HurtTime"); +- this.deathTime = nbt.getShort("DeathTime"); ++ this.deathTime = nbt.getShort("DeathTime"); this.broadcastedDeath = false; // Paper - region threading + this.lastHurtByMobTimestamp = nbt.getInt("HurtByTimestamp"); +- if (nbt.contains("Team", 8)) { ++ if (false && nbt.contains("Team", 8)) { // Paper start - region threading + String s = nbt.getString("Team"); + PlayerTeam scoreboardteam = this.level.getScoreboard().getPlayerTeam(s); + if (!level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper +@@ -3432,7 +3434,7 @@ public abstract class LivingEntity extends Entity { + this.pushEntities(); + this.level.getProfiler().pop(); + // Paper start +- if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { ++ if (((ServerLevel) this.level).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { + if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { + Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); +@@ -4096,7 +4098,7 @@ public abstract class LivingEntity extends Entity { + BlockPos blockposition = new BlockPos(d0, d1, d2); + Level world = this.level; + +- if (world.hasChunkAt(blockposition)) { ++ if (io.papermc.paper.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition) && world.hasChunkAt(blockposition)) { // Paper - region threading + boolean flag2 = false; + + while (!flag2 && blockposition.getY() > world.getMinBuildHeight()) { +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 49b983064ea810382b6112f5dc7f93ba4e5710bd..645de1d871d3ff9dc1bb42842a12b7ae1abbb8c7 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -135,6 +135,14 @@ public abstract class Mob extends LivingEntity { + + public boolean aware = true; // CraftBukkit + ++ // Paper start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.dropLeash(true, true); ++ } ++ // Paper end - region threading ++ + protected Mob(EntityType type, Level world) { + super(type, world); + this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); +@@ -826,12 +834,7 @@ public abstract class Mob extends LivingEntity { + if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { + this.discard(); + } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { +- // Paper start - optimise checkDespawn +- Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).hard() + 1, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper +- if (entityhuman == null) { +- entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0); +- } +- // Paper end - optimise checkDespawn ++ Player entityhuman = this.level.getNearestPlayer(this, -1.0D); // Paper - region threading + + if (entityhuman != null) { + double d0 = entityhuman.distanceToSqr((Entity) this); +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +index 11a101e8ff05fbda5e84018358be02014ca01854..38c93d299f612ac1cc75c7f1ad5c52ca7db7b15f 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +@@ -70,7 +70,7 @@ public class FollowOwnerGoal extends Goal { + + @Override + public boolean canContinueToUse() { +- return this.navigation.isDone() ? false : (this.tamable.isOrderedToSit() ? false : this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance)); ++ return this.navigation.isDone() ? false : (this.tamable.isOrderedToSit() ? false : (this.owner.level == this.level && this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance))); // Paper - region threading - check level + } + + @Override +@@ -93,7 +93,7 @@ public class FollowOwnerGoal extends Goal { + if (--this.timeToRecalcPath <= 0) { + this.timeToRecalcPath = this.adjustedTickDelay(10); + if (!this.tamable.isLeashed() && !this.tamable.isPassenger()) { +- if (this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.owner) || this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) { // Paper - region threading - required in case the player suddenly moves into another dimension + this.teleportToOwner(); + } else { + this.navigation.moveTo((Entity) this.owner, this.speedModifier); +@@ -105,6 +105,11 @@ public class FollowOwnerGoal extends Goal { + + private void teleportToOwner() { + BlockPos blockposition = this.owner.blockPosition(); ++ // Paper start - region threading ++ if (this.owner.isRemoved() || this.owner.level != level) { ++ return; ++ } ++ // Paper end - region threading + + for (int i = 0; i < 10; ++i) { + int j = this.randomIntInclusive(-3, 3); +@@ -135,7 +140,21 @@ public class FollowOwnerGoal extends Goal { + } + to = event.getTo(); + +- this.tamable.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ // Paper start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick ++ // also, use teleportAsync so that crossing region boundaries will not blow up ++ Location finalTo = to; ++ this.tamable.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { ++ if (nmsEntity.level == FollowOwnerGoal.this.level) { ++ nmsEntity.teleportAsync( ++ (net.minecraft.server.level.ServerLevel)nmsEntity.level, ++ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), ++ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), ++ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ } ++ }, null, 1L); ++ // Paper start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick + // CraftBukkit end + this.navigation.stop(); + return true; +diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 97257b450e848f53fdb9b5b7affa57b03ea5f459..9720bc8766cc1dac6892a8a1ef3bf8c238679a3a 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -79,11 +79,11 @@ public abstract class PathNavigation { + } + + public void recomputePath() { +- if (this.level.getGameTime() - this.timeLastRecompute > 20L) { ++ if (this.tick - this.timeLastRecompute > 20L) { // Paper - region threading + if (this.targetPos != null) { + this.path = null; + this.path = this.createPath(this.targetPos, this.reachRange); +- this.timeLastRecompute = this.level.getGameTime(); ++ this.timeLastRecompute = this.tick; // Paper - region threading + this.hasDelayedRecomputation = false; + } + } else { +@@ -198,7 +198,7 @@ public abstract class PathNavigation { + + public boolean moveTo(Entity entity, double speed) { + // Paper start - Pathfinding optimizations +- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { ++ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Paper - region threading + return false; + } + // Paper end +@@ -210,7 +210,7 @@ public abstract class PathNavigation { + return true; + } else { + this.pathfindFailures++; +- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; ++ this.lastFailure = this.tick; // Paper - region threading + return false; + } + // Paper end +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +index e3242cf9a6ad51a23c5781142198dec30c8f376d..2c14888d09168d906c6f4c9e173305c072c3c715 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +@@ -37,7 +37,7 @@ public class TemptingSensor extends Sensor { + + protected void doTick(ServerLevel world, PathfinderMob entity) { + Brain behaviorcontroller = entity.getBrain(); +- Stream stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error ++ Stream stream = world.getLocalPlayers().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error // Paper - region threading + return TemptingSensor.TEMPT_TARGETING.test(entity, entityplayer); + }).filter((entityplayer) -> { + return entity.closerThan(entityplayer, 10.0D); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +index fed09b886f4fa0006d160e5f2abb00dfee45434d..df017df6ad9f09e7b4b9f908a7d76fd79c32f873 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +@@ -22,62 +22,66 @@ import org.slf4j.Logger; + public class VillageSiege implements CustomSpawner { + + private static final Logger LOGGER = LogUtils.getLogger(); +- private boolean hasSetupSiege; +- private VillageSiege.State siegeState; +- private int zombiesToSpawn; +- private int nextSpawnTime; +- private int spawnX; +- private int spawnY; +- private int spawnZ; ++ // Paper - region threading + + public VillageSiege() { +- this.siegeState = VillageSiege.State.SIEGE_DONE; ++ // Paper - region threading + } + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ // Paper start - region threading ++ // check if the spawn pos is no longer owned by this region ++ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE ++ && !io.papermc.paper.util.TickThread.isTickThreadFor(world, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { ++ // can't spawn here, just re-set ++ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionisedWorldData.VillageSiegeState(); ++ } ++ // Paper end - region threading + if (!world.isDay() && spawnMonsters) { + float f = world.getTimeOfDay(0.0F); + + if ((double) f == 0.5D) { +- this.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Paper - region threading + } + +- if (this.siegeState == VillageSiege.State.SIEGE_DONE) { ++ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Paper - region threading + return 0; + } else { +- if (!this.hasSetupSiege) { ++ if (!worldData.villageSiegeState.hasSetupSiege) { // Paper - region threading + if (!this.tryToSetupSiege(world)) { + return 0; + } + +- this.hasSetupSiege = true; ++ worldData.villageSiegeState.hasSetupSiege = true; // Paper - region threading + } + +- if (this.nextSpawnTime > 0) { +- --this.nextSpawnTime; ++ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Paper - region threading ++ --worldData.villageSiegeState.nextSpawnTime; // Paper - region threading + return 0; + } else { +- this.nextSpawnTime = 2; +- if (this.zombiesToSpawn > 0) { ++ worldData.villageSiegeState.nextSpawnTime = 2; // Paper - region threading ++ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Paper - region threading + this.trySpawn(world); +- --this.zombiesToSpawn; ++ --worldData.villageSiegeState.zombiesToSpawn; // Paper - region threading + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Paper - region threading + } + + return 1; + } + } + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; +- this.hasSetupSiege = false; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Paper - region threading ++ worldData.villageSiegeState.hasSetupSiege = false; // Paper - region threading + return 0; + } + } + + private boolean tryToSetupSiege(ServerLevel world) { +- Iterator iterator = world.players().iterator(); ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ Iterator iterator = world.getLocalPlayers().iterator(); // Paper - region threading + + while (iterator.hasNext()) { + Player entityhuman = (Player) iterator.next(); +@@ -89,12 +93,12 @@ public class VillageSiege implements CustomSpawner { + for (int i = 0; i < 10; ++i) { + float f = world.random.nextFloat() * 6.2831855F; + +- this.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); +- this.spawnY = blockposition.getY(); +- this.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); +- if (this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { +- this.nextSpawnTime = 0; +- this.zombiesToSpawn = 20; ++ worldData.villageSiegeState.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Paper - region threading ++ worldData.villageSiegeState.spawnY = blockposition.getY(); // Paper - region threading ++ worldData.villageSiegeState.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Paper - region threading ++ if (this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Paper - region threading ++ worldData.villageSiegeState.nextSpawnTime = 0; // Paper - region threading ++ worldData.villageSiegeState.zombiesToSpawn = 20; // Paper - region threading + break; + } + } +@@ -108,7 +112,8 @@ public class VillageSiege implements CustomSpawner { + } + + private void trySpawn(ServerLevel world) { +- Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Paper - region threading + + if (vec3d != null) { + Zombie entityzombie; +@@ -143,7 +148,7 @@ public class VillageSiege implements CustomSpawner { + return null; + } + +- private static enum State { ++ public static enum State { // Paper - region threading + + SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE; + +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 9be85eb0abec02bc0e0eded71c34ab1c565c63e7..1348a64e7530d449fb9177a4dfc74cdc275b5942 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -48,11 +48,13 @@ public class PoiManager extends SectionStorage { + } + + protected void updateDistanceTracking(long section) { ++ synchronized (this.villageDistanceTracker) { // Paper - region threading + if (this.isVillageCenter(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } ++ } // Paper - region threading + } + // Paper end - rewrite chunk system + +@@ -215,8 +217,10 @@ public class PoiManager extends SectionStorage { + } + + public int sectionsToVillage(SectionPos pos) { ++ synchronized (this.villageDistanceTracker) { // Paper - region threading + this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util + return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util ++ } // Paper - region threading + } + + boolean isVillageCenter(long pos) { +@@ -230,7 +234,9 @@ public class PoiManager extends SectionStorage { + + @Override + public void tick(BooleanSupplier shouldKeepTicking) { ++ synchronized (this.villageDistanceTracker) { // Paper - region threading + this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system ++ } // Paper - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java +index 0114c1cf3b6b0500149a77ebc190cb7fa2832184..de7f839d6cf68080ca43f058a0635b624a927a15 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java +@@ -366,7 +366,7 @@ public class Cat extends TamableAnimal implements VariantHolder { + }); + ServerLevel worldserver = world.getLevel(); + +- if (worldserver.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, world).isValid()) { // Paper - fix deadlock ++ if (world.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - fix deadlock // Paper - region threading - properly fix this + this.setVariant((CatVariant) BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK)); + this.setPersistenceRequired(); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 25503678e7d049a8b3172cfad8a5606958c32302..a2b11e8b87bdfcb6157b26a67ccec93293d0ff67 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -336,9 +336,9 @@ public class Turtle extends Animal { + + @Override + public void thunderHit(ServerLevel world, LightningBolt lightning) { +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = lightning; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(lightning); // CraftBukkit // Paper - region threading + this.hurt(DamageSource.LIGHTNING_BOLT, Float.MAX_VALUE); +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Paper - region threading + } + + private static class TurtleMoveControl extends MoveControl { +diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +index eacb8a407fe99af2c13f23c12b5544696bda8890..fb50ac26690182b1b173471ea2c66d7f32bcbdfd 100644 +--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -292,9 +292,9 @@ public class FallingBlockEntity extends Entity { + float f2 = (float) Math.min(Mth.floor((float) i * this.fallDamagePerDistance), this.fallDamageMax); + + this.level.getEntities((Entity) this, this.getBoundingBox(), predicate).forEach((entity) -> { +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Paper - region threading + entity.hurt(damagesource1, f2); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Paper - region threading + }); + boolean flag = this.blockState.is(BlockTags.ANVIL); + +diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +index f0ccdfbd7d7be8c6e302609accf8fe9cac8885c4..e93bc71c212dbf3a35c6ee7dff91e1c59994c812 100644 +--- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +@@ -50,7 +50,7 @@ public class ItemEntity extends Entity { + @Nullable + private UUID owner; + public final float bobOffs; +- private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit ++ //private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit // Paper - region threading + public boolean canMobPickup = true; // Paper + private int despawnRate = -1; // Paper + public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper +@@ -116,13 +116,11 @@ public class ItemEntity extends Entity { + this.discard(); + } else { + super.tick(); +- // CraftBukkit start - Use wall time for pickup and despawn timers +- int elapsedTicks = MinecraftServer.currentTick - this.lastTick; +- if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks; +- this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0 +- if (this.age != -32768) this.age += elapsedTicks; +- this.lastTick = MinecraftServer.currentTick; +- // CraftBukkit end ++ // Paper start - region threading - restore original timers ++ if (this.pickupDelay > 0 && this.pickupDelay != 32767) { ++ --this.pickupDelay; ++ } ++ // Paper end - region threading - restore original timers + + this.xo = this.getX(); + this.yo = this.getY(); +@@ -176,11 +174,11 @@ public class ItemEntity extends Entity { + this.mergeWithNeighbours(); + } + +- /* CraftBukkit start - moved up ++ // Paper - region threading - restore original timers + if (this.age != -32768) { + ++this.age; + } +- // CraftBukkit end */ ++ // Paper - region threading - restore original timers + + this.hasImpulse |= this.updateInWaterStateAndDoFluidPushing(); + if (!this.level.isClientSide) { +@@ -207,13 +205,14 @@ public class ItemEntity extends Entity { + // Spigot start - copied from above + @Override + public void inactiveTick() { +- // CraftBukkit start - Use wall time for pickup and despawn timers +- int elapsedTicks = MinecraftServer.currentTick - this.lastTick; +- if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks; +- this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0 +- if (this.age != -32768) this.age += elapsedTicks; +- this.lastTick = MinecraftServer.currentTick; +- // CraftBukkit end ++ // Paper start - region threading - restore original timers ++ if (this.pickupDelay > 0 && this.pickupDelay != 32767) { ++ --this.pickupDelay; ++ } ++ if (this.age != -32768) { ++ ++this.age; ++ } ++ // Paper end - region threading - restore original timers + + if (!this.level.isClientSide && this.age >= this.despawnRate) { // Spigot // Paper + // CraftBukkit start - fire ItemDespawnEvent +@@ -514,14 +513,20 @@ public class ItemEntity extends Entity { + return false; + } + ++ // Paper start - region threading ++ @Override ++ public void postChangeDimension() { ++ super.postChangeDimension(); ++ this.mergeWithNeighbours(); ++ } ++ // Paper end - region threading ++ + @Nullable + @Override + public Entity changeDimension(ServerLevel destination) { + Entity entity = super.changeDimension(destination); + +- if (!this.level.isClientSide && entity instanceof ItemEntity) { +- ((ItemEntity) entity).mergeWithNeighbours(); +- } ++ if (entity != null) entity.postChangeDimension(); // Paper - region threading - move to post change + + return entity; + } +diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +index bedee2c93bd0aff148f93dcf111e0fc3d9bce4a0..b58bf927e1527147ffbb23a14d906aff22870908 100644 +--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +@@ -59,7 +59,7 @@ public class PrimedTnt extends Entity { + + @Override + public void tick() { +- if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.spigotConfig.currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot ++ if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.getCurrentWorldData().currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Paper - region threading + if (!this.isNoGravity()) { + this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.04D, 0.0D)); + } +@@ -101,7 +101,7 @@ public class PrimedTnt extends Entity { + */ + // Send position and velocity updates to nearby players on every tick while the TNT is in water. + // This does pretty well at keeping their clients in sync with the server. +- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel)this.level).getChunkSource().chunkMap.entityMap.get(this.getId()); ++ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.tracker; // Paper - region threading + if (ete != null) { + net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); + net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = new net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket(this); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +index 9976205537cfe228735687f1e9c52c74ac025690..e1e93b48a7ea11f13a0f635eea4a1745af0890d5 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java +@@ -94,7 +94,7 @@ public class Zombie extends Monster { + private boolean canBreakDoors; + private int inWaterTime; + public int conversionTime; +- private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field ++ // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Paper - region threading - restore original timers + private boolean shouldBurnInDay = true; // Paper + + public Zombie(EntityType type, Level world) { +@@ -217,10 +217,7 @@ public class Zombie extends Monster { + public void tick() { + if (!this.level.isClientSide && this.isAlive() && !this.isNoAi()) { + if (this.isUnderWaterConverting()) { +- // CraftBukkit start - Use wall time instead of ticks for conversion +- int elapsedTicks = MinecraftServer.currentTick - this.lastTick; +- this.conversionTime -= elapsedTicks; +- // CraftBukkit end ++ --this.conversionTime; // Paper - region threading - restore original timers + if (this.conversionTime < 0) { + this.doUnderWaterConversion(); + } +@@ -237,7 +234,7 @@ public class Zombie extends Monster { + } + + super.tick(); +- this.lastTick = MinecraftServer.currentTick; // CraftBukkit ++ //this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Paper - region threading - restore original timers + } + + @Override +@@ -276,7 +273,7 @@ public class Zombie extends Monster { + } + // Paper end + public void startUnderWaterConversion(int ticksUntilWaterConversion) { +- this.lastTick = MinecraftServer.currentTick; // CraftBukkit ++ // Paper - region threading - restore original timers + this.conversionTime = ticksUntilWaterConversion; + this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, true); + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +index 71a36cf9b976443cca9ab63cd0eb23253f638562..b6d9cf729c732f3954aacdced6e8b7661c65f606 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -70,7 +70,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + @Nullable + private CompoundTag tradeOffers; + private int villagerXp; +- private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field ++ // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Paper - region threading - restore original timers + + public ZombieVillager(EntityType type, Level world) { + super(type, world); +@@ -145,10 +145,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + public void tick() { + if (!this.level.isClientSide && this.isAlive() && this.isConverting()) { + int i = this.getConversionProgress(); +- // CraftBukkit start - Use wall time instead of ticks for villager conversion +- int elapsedTicks = MinecraftServer.currentTick - this.lastTick; +- i *= elapsedTicks; +- // CraftBukkit end ++ // Paper - region threading - restore original timers + + this.villagerConversionTime -= i; + if (this.villagerConversionTime <= 0) { +@@ -157,7 +154,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { + } + + super.tick(); +- this.lastTick = MinecraftServer.currentTick; // CraftBukkit ++ // Paper - region threading - restore original timers + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index ca96b893e22de3ae7c11d5cded51edf70bdcb6f2..2122761e70931bf80d859f8c13fc6757acbeca6b 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -213,10 +213,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + this.readInventoryFromTag(nbt); + } + ++ // Paper start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopTrading(); ++ } ++ // Paper end - region threading ++ + @Nullable + @Override + public Entity changeDimension(ServerLevel destination) { +- this.stopTrading(); ++ this.preChangeDimension(); // Paper - region threading - move into preChangeDimension + return super.changeDimension(destination); + } + +diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +index 5f407535298a31a34cfe114dd863fd6a9b977707..a7937515a94bcaf981dfd7d525eed4cac9b85639 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +@@ -21,17 +21,18 @@ import net.minecraft.world.phys.AABB; + + public class CatSpawner implements CustomSpawner { + private static final int TICK_DELAY = 1200; +- private int nextTick; ++ //private int nextTick; // Paper - region threading + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + if (spawnAnimals && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { +- --this.nextTick; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ --worldData.catSpawnerNextTick; // Paper - region threading ++ if (worldData.catSpawnerNextTick > 0) { // Paper - region threading + return 0; + } else { +- this.nextTick = 1200; +- Player player = world.getRandomPlayer(); ++ worldData.catSpawnerNextTick = 1200; // Paper - region threading ++ Player player = world.getRandomLocalPlayer(); // Paper - region threading + if (player == null) { + return 0; + } else { +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index 18eac340386a396c9850f53f30d20a41c1437788..bbae576e1b360809ee5b8d0ba2da7456d042b622 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -710,6 +710,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension()); + + if (worldserver != null) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( // Paper - region threading ++ worldserver, globalpos.pos().getX() >> 4, globalpos.pos().getZ() >> 4, () -> { // Paper - region threading + PoiManager villageplace = worldserver.getPoiManager(); + Optional> optional = villageplace.getType(globalpos.pos()); + BiPredicate> bipredicate = (BiPredicate) Villager.POI_MEMORIES.get(pos); +@@ -718,6 +720,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + villageplace.release(globalpos.pos()); + DebugPackets.sendPoiTicketCountPacket(worldserver, globalpos.pos()); + } ++ }); // Paper - region threading + + } + }); +diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +index 0ae8e9134a3671cdf2a480cd4dd6598653e261ab..818cea3b1d00879fea4346d8e1c761a28cb9e206 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -32,16 +32,14 @@ public class WanderingTraderSpawner implements CustomSpawner { + private static final int SPAWN_CHANCE_INCREASE = 25; + private static final int SPAWN_ONE_IN_X_CHANCE = 10; + private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; +- private final RandomSource random = RandomSource.create(); ++ private final RandomSource random = new net.minecraft.world.entity.Entity.RandomRandomSource(); // Paper - region threading + private final ServerLevelData serverLevelData; +- private int tickDelay; +- private int spawnDelay; +- private int spawnChance; ++ // Paper - region threading + + public WanderingTraderSpawner(ServerLevelData properties) { + this.serverLevelData = properties; + // Paper start +- this.tickDelay = Integer.MIN_VALUE; ++ //this.tickDelay = Integer.MIN_VALUE; // Paper - region threading - moved to regionisedworlddata + //this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value + //this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value + //if (this.spawnDelay == 0 && this.spawnChance == 0) { +@@ -56,36 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner { + + @Override + public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading + // Paper start +- if (this.tickDelay == Integer.MIN_VALUE) { +- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; +- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Paper - region threading ++ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Paper - region threading ++ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Paper - region threading ++ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Paper - region threading + } + if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { + return 0; +- } else if (this.tickDelay - 1 > 0) { +- this.tickDelay = this.tickDelay - 1; ++ } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Paper - region threading ++ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Paper - region threading + return 0; + } else { +- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; ++ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Paper - region threading ++ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Paper - region threading + //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways +- if (this.spawnDelay > 0) { ++ if (worldData.wanderingTraderSpawnDelay > 0) { // Paper - region threading + return 0; + } else { +- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; ++ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Paper - region threading + if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { + return 0; + } else { +- int i = this.spawnChance; ++ int i = worldData.wanderingTraderSpawnChance; // Paper - region threading + +- this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); ++ worldData.wanderingTraderSpawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Paper - region threading + //this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways + if (this.random.nextInt(100) > i) { + return 0; + } else if (this.spawn(world)) { +- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Paper - region threading + // Paper end + return 1; + } else { +@@ -97,7 +96,7 @@ public class WanderingTraderSpawner implements CustomSpawner { + } + + private boolean spawn(ServerLevel world) { +- ServerPlayer entityplayer = world.getRandomPlayer(); ++ ServerPlayer entityplayer = world.getRandomLocalPlayer(); // Paper - region threading + + if (entityplayer == null) { + return true; +@@ -127,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner { + this.tryToSpawnLlamaFor(world, entityvillagertrader, 4); + } + +- this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); ++ //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Paper - region threading - doesn't appear to be used anywhere, so avoid the race condition here... + // entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent + entityvillagertrader.setWanderTarget(blockposition1); + entityvillagertrader.restrictTo(blockposition1, 16); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +index c7265a650a5d6bdc42d41c5c90cad401d7f1c99d..793ce02233202542ca7f421e0ba8e7e5a3dc3763 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +@@ -128,9 +128,9 @@ public class EvokerFangs extends Entity { + + if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) { + if (entityliving1 == null) { +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = this; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Paper - region threading + target.hurt(DamageSource.MAGIC, 6.0F); +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Paper - region threading + } else { + if (entityliving1.isAlliedTo((Entity) target)) { + return; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +index 5406925cd66f46ab8744123c670d72cea7bfc3a1..39ba77db0767a09798fe17156b2dffeca11af1c3 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +@@ -130,6 +130,10 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + + }); + } ++ if (this.attachedToEntity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(this.attachedToEntity)) { // Paper start - region threading ++ this.attachedToEntity = null; ++ } ++ // Paper end - region threading + + if (this.attachedToEntity != null) { + if (this.attachedToEntity.isFallFlying()) { +@@ -241,9 +245,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + + if (f > 0.0F) { + if (this.attachedToEntity != null) { +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Paper - region threading + this.attachedToEntity.hurt(DamageSource.fireworks(this, this.getOwner()), 5.0F + (float) (nbttaglist.size() * 2)); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Paper - region threading + } + + double d0 = 5.0D; +@@ -270,9 +274,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + if (flag) { + float f1 = f * (float) Math.sqrt((5.0D - (double) this.distanceTo(entityliving)) / 5.0D); + +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Paper - region threading + entityliving.hurt(DamageSource.fireworks(this, this.getOwner()), f1); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Paper - region threading + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index 66476b33cede1e44db5ec166a0cea81f82ffe47a..e67ebc2284dc18b74e098694549f8c9850112bcf 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -52,8 +52,19 @@ public abstract class Projectile extends Entity { + + } + ++ // Paper start - region threading ++ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. ++ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. + @Nullable + public Entity getOwner() { ++ Entity ret = this.getOwnerRaw(); ++ return io.papermc.paper.util.TickThread.isTickThreadFor(ret) ? ret : null; ++ } ++ // Paper end - region threading ++ ++ @Nullable ++ public Entity getOwnerRaw() { // Paper - region threading ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Paper - region threading + if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { + return this.cachedOwner; + } else if (this.ownerUUID != null && this.level instanceof ServerLevel) { +@@ -273,6 +284,6 @@ public abstract class Projectile extends Entity { + public boolean mayInteract(Level world, BlockPos pos) { + Entity entity = this.getOwner(); + +- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return entity instanceof Player && io.papermc.paper.util.TickThread.isTickThreadFor(entity) ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Paper - region threading + } + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +index 00ac1cdc4734cc57f15433c5c6e7a3a545739d33..3f26008055462987bd4c0e38303e0dcab0315fa2 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -23,7 +23,7 @@ public class SmallFireball extends Fireball { + public SmallFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ) { + super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world); + // CraftBukkit start +- if (this.getOwner() != null && this.getOwner() instanceof Mob) { ++ if (owner != null && owner instanceof Mob) { // Paper - region threading + isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index f224ebbc0efefddede43d87f0300c014077b9931..05c4cad5c3cec3a1316337fcd89eedd875deb0bc 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -44,6 +44,62 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + entityHitResult.getEntity().hurt(DamageSource.thrown(this, this.getOwner()), 0.0F); + } + ++ // Paper start - region threading ++ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { ++ // ignore retired callback, in those cases we do not want to teleport ++ source.getBukkitEntity().taskScheduler.schedule( ++ (Entity entity) -> { ++ // source is now an invalid reference, do not use it, use the entity parameter ++ ++ if (entity.getLevel() != checkWorld) { ++ // cannot teleport cross-world ++ return; ++ } ++ if (entity.isVehicle()) { ++ // cannot teleport vehicles ++ return; ++ } ++ // dismount from any vehicles, so we can teleport and to prevent desync ++ if (entity.isPassenger()) { ++ entity.stopRiding(); ++ } ++ ++ // reset fall damage so that if the entity was falling they do not instantly die ++ entity.resetFallDistance(); ++ ++ entity.teleportAsync( ++ checkWorld, to, null, null, null, ++ PlayerTeleportEvent.TeleportCause.ENDER_PEARL, ++ // chunk could have been unloaded ++ Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ (Entity teleported) -> { ++ // entity is now an invalid reference, do not use it, instead use teleported ++ if (teleported instanceof ServerPlayer player) { ++ // connection teleport is already done ++ ServerLevel world = player.getLevel(); ++ ++ // endermite spawn chance ++ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ Endermite entityendermite = EntityType.ENDERMITE.create(world); ++ ++ if (entityendermite != null) { ++ entityendermite.moveTo(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); ++ world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); ++ } ++ } ++ ++ // damage player ++ player.hurt(DamageSource.FALL, 5.0F); ++ } ++ } ++ ); ++ }, ++ null, ++ 1L ++ ); ++ } ++ // Paper end - region threading ++ + @Override + protected void onHit(HitResult hitResult) { + super.onHit(hitResult); +@@ -53,6 +109,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + } + + if (!this.level.isClientSide && !this.isRemoved()) { ++ // Paper start - region threading ++ if (true) { ++ // we can't fire events, because we do not actually know where the other entity is located ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this)) { ++ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); ++ } ++ Entity entity = this.getOwnerRaw(); ++ if (entity != null) { ++ attemptTeleport(entity, (ServerLevel)this.getLevel(), this.position()); ++ } ++ this.discard(); ++ return; ++ } ++ // Paper end - region threading + Entity entity = this.getOwner(); + + if (entity instanceof ServerPlayer) { +@@ -84,9 +154,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); +- CraftEventFactory.entityDamage = this; ++ CraftEventFactory.entityDamageRT.set(this); // Paper - region threading + entity.hurt(DamageSource.FALL, 5.0F); +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // Paper - region threading + } + // CraftBukkit end + } +@@ -112,6 +182,14 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + } + ++ // Paper start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ // Don't change the owner here, since the tick logic will consider it anyways. ++ } ++ // Paper end - region threading ++ + @Nullable + @Override + public Entity changeDimension(ServerLevel destination) { +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java +index 08b18428e867baf14f551beb72e3875b0c420639..25a281c2944d79d7b91d8bdfa535b3cccf565668 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java +@@ -527,7 +527,7 @@ public class Raid { + boolean flag = true; + Collection collection = this.raidEvent.getPlayers(); + long i = this.random.nextLong(); +- Iterator iterator = this.level.players().iterator(); ++ Iterator iterator = this.level.getLocalPlayers().iterator(); // Paper - region threading + + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java +index e5ccbaf72f29731f1d1aa939b9297b644a408cd4..27318f3a879f6798af4b4ae96d597958266cd7da 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java +@@ -91,7 +91,7 @@ public abstract class Raider extends PatrollingMonster { + + if (this.canJoinRaid()) { + if (raid == null) { +- if (this.level.getGameTime() % 20L == 0L) { ++ if (this.level.getRedstoneGameTime() % 20L == 0L) { // Paper - region threading + Raid raid1 = ((ServerLevel) this.level).getRaidAt(this.blockPosition()); + + if (raid1 != null && Raids.canJoinRaid(this, raid1)) { +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +index 70f1916185b79bbb9f033f4ef8119d7b17a13ef8..54d55e8827f4ab286fca722f199aac42cddab8d2 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +@@ -155,7 +155,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper + + // Paper start + public void immunize() { +- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 20); + } + // Paper end + +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index 9c8604376228c02f8bbd9a15673fbdf5097e7cb2..ed784d9b434a17e9806ec413359c0f8047f5cc7a 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -63,7 +63,7 @@ public class ArmorItem extends Item implements Wearable { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 6860096cb8c0deecc9c1d87543d1128fb95fd2d4..0cf04eaaaa28da545038c181cf1d935dc02d43bf 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -333,6 +333,7 @@ public final class ItemStack { + } + + public InteractionResult useOn(UseOnContext itemactioncontext, InteractionHand enumhand) { // CraftBukkit - add hand ++ + net.minecraft.world.entity.player.Player entityhuman = itemactioncontext.getPlayer(); + BlockPos blockposition = itemactioncontext.getClickedPos(); + BlockInWorld shapedetectorblock = new BlockInWorld(itemactioncontext.getLevel(), blockposition, false); +@@ -344,12 +345,13 @@ public final class ItemStack { + CompoundTag oldData = this.getTagClone(); + int oldCount = this.getCount(); + ServerLevel world = (ServerLevel) itemactioncontext.getLevel(); ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading + + if (!(this.getItem() instanceof BucketItem/* || this.getItem() instanceof SolidBucketItem*/)) { // if not bucket // Paper - capture block states for snow buckets +- world.captureBlockStates = true; ++ worldData.captureBlockStates = true; // Paper - region threading + // special case bonemeal + if (this.getItem() == Items.BONE_MEAL) { +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Paper - region threading + } + } + Item item = this.getItem(); +@@ -358,14 +360,14 @@ public final class ItemStack { + int newCount = this.getCount(); + this.setCount(oldCount); + this.setTagClone(oldData); +- world.captureBlockStates = false; +- if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) { +- world.captureTreeGeneration = false; ++ worldData.captureBlockStates = false; // Paper - region threading ++ if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Paper - region threading ++ world.getCurrentWorldData().captureTreeGeneration = false; // Paper - region threading + Location location = new Location(world.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ()); + TreeType treeType = SaplingBlock.treeType; + SaplingBlock.treeType = null; +- List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + StructureGrowEvent structureEvent = null; + if (treeType != null) { + boolean isBonemeal = this.getItem() == Items.BONE_MEAL; +@@ -392,12 +394,12 @@ public final class ItemStack { + SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + return enuminteractionresult; + } +- world.captureTreeGeneration = false; ++ worldData.captureTreeGeneration = false; // Paper - region threading + + if (entityhuman != null && enuminteractionresult.shouldAwardStats()) { + org.bukkit.event.block.BlockPlaceEvent placeEvent = null; +- List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + if (blocks.size() > 1) { + placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(world, entityhuman, enumhand, blocks, blockposition.getX(), blockposition.getY(), blockposition.getZ()); + } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - don't call event twice for snow buckets +@@ -408,13 +410,13 @@ public final class ItemStack { + enuminteractionresult = InteractionResult.FAIL; // cancel placement + // PAIL: Remove this when MC-99075 fixed + placeEvent.getPlayer().updateInventory(); +- world.capturedTileEntities.clear(); // Paper - clear out tile entities as chests and such will pop loot ++ worldData.capturedTileEntities.clear(); // Paper - clear out tile entities as chests and such will pop loot // Paper - region threading + // revert back all captured blocks +- world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 ++ worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Paper - region threading + for (BlockState blockstate : blocks) { + blockstate.update(true, false); + } +- world.preventPoiUpdated = false; ++ worldData.preventPoiUpdated = false; // Paper - region threading + + // Brute force all possible updates + BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); +@@ -429,7 +431,7 @@ public final class ItemStack { + this.setCount(newCount); + } + +- for (Map.Entry e : world.capturedTileEntities.entrySet()) { ++ for (Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Paper - region threading + world.setBlockEntity(e.getValue()); + } + +@@ -490,8 +492,8 @@ public final class ItemStack { + entityhuman.awardStat(Stats.ITEM_USED.get(item)); + } + } +- world.capturedTileEntities.clear(); +- world.capturedBlockStates.clear(); ++ worldData.capturedTileEntities.clear(); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + // CraftBukkit end + + return enuminteractionresult; +diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java +index c6d2f764efa9b8bec730bbe757d480e365b25ccc..6349e2b658936d96e7e61cc075c9e622efe9f2c4 100644 +--- a/src/main/java/net/minecraft/world/item/MinecartItem.java ++++ b/src/main/java/net/minecraft/world/item/MinecartItem.java +@@ -67,7 +67,7 @@ public class MinecartItem extends Item { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // Paper - region threading + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +index 888936385196a178ab8b730fd5e4fff4a5466428..4ed13a0d861be38b90e3876400b62437758c9e71 100644 +--- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java ++++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +@@ -111,6 +111,7 @@ public abstract class BaseCommandBlock implements CommandSource { + } + + public boolean performCommand(Level world) { ++ if (true) return false; // Paper - region threading + if (!world.isClientSide && world.getGameTime() != this.lastExecution) { + if ("Searge".equalsIgnoreCase(this.command)) { + this.lastOutput = Component.literal("#itzlipofutzli"); +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 3b959f42d958bf0f426853aee56753d6c455fcdb..4b7c27db1eef71fef6bdea797b6a935802537784 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -38,6 +38,12 @@ public interface EntityGetter { + return this.getEntities(EntityTypeTest.forClass(entityClass), box, predicate); + } + ++ // Paper start - region threading ++ default List getLocalPlayers() { ++ return java.util.Collections.emptyList(); ++ } ++ // Paper end - region threading ++ + List players(); + + default List getEntities(@Nullable Entity except, AABB box) { +@@ -92,7 +98,7 @@ public interface EntityGetter { + double d = -1.0D; + Player player = null; + +- for(Player player2 : this.players()) { ++ for(Player player2 : this.getLocalPlayers()) { // Paper - region threading + if (targetPredicate == null || targetPredicate.test(player2)) { + double e = player2.distanceToSqr(x, y, z); + if ((maxDistance < 0.0D || e < maxDistance * maxDistance) && (d == -1.0D || e < d)) { +@@ -113,7 +119,7 @@ public interface EntityGetter { + default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { + com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); + +- for (Player human : this.players()) { ++ for (Player human : this.getLocalPlayers()) { // Paper - region threading + if (predicate == null || predicate.test(human)) { + double distanceSquared = human.distanceToSqr(x, y, z); + +@@ -140,7 +146,7 @@ public interface EntityGetter { + + // Paper start + default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Paper - region threading + if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check + double distanceSqr = player.distanceToSqr(x, y, z); + if (range < 0.0D || distanceSqr < range * range) { +@@ -153,7 +159,7 @@ public interface EntityGetter { + // Paper end + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { +- for(Player player : this.players()) { ++ for(Player player : this.getLocalPlayers()) { // Paper - region threading + if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { + double d = player.distanceToSqr(x, y, z); + if (range < 0.0D || d < range * range) { +@@ -167,17 +173,17 @@ public interface EntityGetter { + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity) { +- return this.getNearestEntity(this.players(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); // Paper - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, x, y, z); // Paper - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetPredicate, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetPredicate, (LivingEntity)null, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, (LivingEntity)null, x, y, z); // Paper - region threading + } + + @Nullable +@@ -208,7 +214,7 @@ public interface EntityGetter { + default List getNearbyPlayers(TargetingConditions targetPredicate, LivingEntity entity, AABB box) { + List list = Lists.newArrayList(); + +- for(Player player : this.players()) { ++ for(Player player : this.getLocalPlayers()) { // Paper - region threading + if (box.contains(player.getX(), player.getY(), player.getZ()) && targetPredicate.test(entity, player)) { + list.add(player); + } +@@ -234,8 +240,7 @@ public interface EntityGetter { + + @Nullable + default Player getPlayerByUUID(UUID uuid) { +- for(int i = 0; i < this.players().size(); ++i) { +- Player player = this.players().get(i); ++ for(Player player : this.getLocalPlayers()) { // Paper - region threading + if (uuid.equals(player.getUUID())) { + return player; + } +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index a213f4098859858a73ddd601bbe8c7511972e0d5..aeadc22e0df2d759252f5c73f9a79b7557991b10 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -246,7 +246,7 @@ public class Explosion { + continue; + } + +- CraftEventFactory.entityDamage = this.source; ++ CraftEventFactory.entityDamageRT.set(this.source); // Paper - region threading + entity.lastDamageCancelled = false; + + if (entity instanceof EnderDragon) { +@@ -259,7 +259,7 @@ public class Explosion { + entity.hurt(this.getDamageSource(), (float) ((int) ((d13 * d13 + d13) / 2.0D * 7.0D * (double) f2 + 1.0D))); + } + +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // Paper - region threading + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled + continue; + } +@@ -503,17 +503,10 @@ public class Explosion { + } + // Paper start - Optimize explosions + private float getBlockDensity(Vec3 vec3d, Entity entity) { +- if (!this.level.paperConfig().environment.optimizeExplosions) { ++ if (true || !this.level.paperConfig().environment.optimizeExplosions) { // Paper - region threading + return getSeenPercent(vec3d, entity); + } +- CacheKey key = new CacheKey(this, entity.getBoundingBox()); +- Float blockDensity = this.level.explosionDensityCache.get(key); +- if (blockDensity == null) { +- blockDensity = getSeenPercent(vec3d, entity); +- this.level.explosionDensityCache.put(key, blockDensity); +- } +- +- return blockDensity; ++ return 0.0f; // Paper - region threading + } + + static class CacheKey { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 60003ff929f7ac6b34f9230c53ccbd54dc9e176b..47871e3aaa54fad6f01ea31137a1fc14d7f2bcd8 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -116,10 +116,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public static final int TICKS_PER_DAY = 24000; + public static final int MAX_ENTITY_SPAWN_Y = 20000000; + public static final int MIN_ENTITY_SPAWN_Y = -20000000; +- protected final List blockEntityTickers = Lists.newArrayList(); public final int getTotalTileEntityTickers() { return this.blockEntityTickers.size(); } // Paper +- protected final NeighborUpdater neighborUpdater; +- private final List pendingBlockEntityTickers = Lists.newArrayList(); +- private boolean tickingBlockEntities; ++ //protected final List blockEntityTickers = Lists.newArrayList(); public final int getTotalTileEntityTickers() { return this.blockEntityTickers.size(); } // Paper // Paper - region threading ++ public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; ++ //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Paper - region threading ++ //private boolean tickingBlockEntities; // Paper - region threading + public final Thread thread; + private final boolean isDebug; + private int skyDarken; +@@ -129,7 +129,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public float rainLevel; + protected float oThunderLevel; + public float thunderLevel; +- public final RandomSource random = RandomSource.create(); ++ public final RandomSource random = new Entity.RandomRandomSource(); // Paper - region threading + /** @deprecated */ + @Deprecated + private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); +@@ -141,7 +141,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + private final WorldBorder worldBorder; + private final BiomeManager biomeManager; + private final ResourceKey dimension; +- private long subTickCount; ++ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Paper - region threading + + // CraftBukkit start Added the following + private final CraftWorld world; +@@ -150,20 +150,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public org.bukkit.generator.ChunkGenerator generator; + public static final boolean DEBUG_ENTITIES = Boolean.getBoolean("debug.entities"); // Paper + +- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 +- public boolean captureBlockStates = false; +- public boolean captureTreeGeneration = false; +- public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper +- public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper +- public List captureDrops; ++ // Paper - region threading - moved to regionised data + public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); +- // Paper start +- public int wakeupInactiveRemainingAnimals; +- public int wakeupInactiveRemainingFlying; +- public int wakeupInactiveRemainingMonsters; +- public int wakeupInactiveRemainingVillagers; +- // Paper end +- public boolean populating; ++ // Paper - region threading - moved to regionised data ++ // Paper - region threading + public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot + // Paper start + private final io.papermc.paper.configuration.WorldConfiguration paperConfig; +@@ -177,9 +167,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; + private org.spigotmc.TickLimiter tileLimiter; +- private int tileTickPosition; +- public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions +- public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here ++ //private int tileTickPosition; // Paper - region threading ++ //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Paper - region threading ++ //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here // Paper - region threading + + // Paper start - fix and optimise world upgrading + // copied from below +@@ -223,7 +213,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + List ret = new java.util.ArrayList<>(); + double maxRangeSquared = maxRange * maxRange; + +- for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { ++ for (net.minecraft.server.level.ServerPlayer player : (List)this.getLocalPlayers()) { // Paper - region threading + if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) { + if (predicate == null || predicate.test(player)) { + ret.add(player); +@@ -239,7 +229,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + net.minecraft.server.level.ServerPlayer closest = null; + double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + +- for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { ++ for (net.minecraft.server.level.ServerPlayer player : (List)this.getLocalPlayers()) { // Paper - region threading + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) { + closest = player; +@@ -270,6 +260,24 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + ++ // Paper start - region ticking ++ public final io.papermc.paper.threadedregions.RegionisedData worldRegionData ++ = new io.papermc.paper.threadedregions.RegionisedData<>( ++ (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionisedWorldData((ServerLevel)Level.this), ++ io.papermc.paper.threadedregions.RegionisedWorldData.REGION_CALLBACK ++ ); ++ public volatile io.papermc.paper.threadedregions.RegionisedServer.WorldLevelData tickData; ++ ++ public io.papermc.paper.threadedregions.RegionisedWorldData getCurrentWorldData() { ++ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); ++ } ++ ++ @Override ++ public List getLocalPlayers() { ++ return this.getCurrentWorldData().getLocalPlayers(); ++ } ++ // Paper end - region ticking ++ + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper +@@ -313,7 +321,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.thread = Thread.currentThread(); + this.biomeManager = new BiomeManager(this, i); + this.isDebug = flag1; +- this.neighborUpdater = new CollectingNeighborUpdater(this, j); ++ this.neighbourUpdateMax = j; // Paper - region threading + // CraftBukkit start + this.getWorldBorder().world = (ServerLevel) this; + // From PlayerList.setPlayerFileData +@@ -452,8 +460,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + @Nullable + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + // CraftBukkit start - tree generation +- if (captureTreeGeneration) { +- CraftBlockState previous = capturedBlockStates.get(blockposition); ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Paper - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(blockposition); // Paper - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -514,16 +522,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = this.getCurrentWorldData(); // Paper - region threading + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { ++ if (worldData.captureTreeGeneration) { // Paper - region threading + // Paper start + BlockState type = getBlockState(pos); + if (!type.isDestroyable()) return false; + // Paper end +- CraftBlockState blockstate = this.capturedBlockStates.get(pos); ++ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Paper - region threading + if (blockstate == null) { + blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Paper - region threading + } + blockstate.setData(state); + return true; +@@ -539,10 +548,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + // CraftBukkit start - capture blockstates + boolean captured = false; +- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { ++ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Paper - region threading + CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot + blockstate.setFlag(flags); // Paper - set flag +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Paper - region threading + captured = true; + } + // CraftBukkit end +@@ -552,8 +561,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + if (iblockdata1 == null) { + // CraftBukkit start - remove blockstate if failed (or the same) +- if (this.captureBlockStates && captured) { +- this.capturedBlockStates.remove(pos); ++ if (worldData.captureBlockStates && captured) { // Paper - region threading ++ worldData.capturedBlockStates.remove(pos); // Paper - region threading + } + // CraftBukkit end + return false; +@@ -596,7 +605,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + */ + + // CraftBukkit start +- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates ++ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Paper - region threading + // Modularize client and physic updates + // Spigot start + try { +@@ -645,7 +654,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // CraftBukkit start + iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam + CraftWorld world = ((ServerLevel) this).getWorld(); +- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper ++ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper // Paper - region threading + BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); + this.getCraftServer().getPluginManager().callEvent(event); + +@@ -659,7 +668,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + // CraftBukkit start - SPIGOT-5710 +- if (!this.preventPoiUpdated) { ++ if (!this.getCurrentWorldData().preventPoiUpdated) { // Paper - region threading + this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); + } + // CraftBukkit end +@@ -738,7 +747,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public void neighborShapeChanged(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) { +- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); ++ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); // Paper - region threading + } + + @Override +@@ -763,11 +772,24 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return this.getChunkSource().getLightEngine(); + } + ++ // Paper start - region threading ++ @Nullable ++ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { ++ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ if (chunk != null) { ++ return chunk.getBlockState(pos); ++ } ++ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); ++ return chunk.getBlockState(pos); ++ } ++ // Paper end - region threading ++ + @Override + public BlockState getBlockState(BlockPos pos) { + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Paper - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Paper - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -858,7 +880,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + + public void addBlockEntityTicker(TickingBlockEntity ticker) { +- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Paper - regionised ticking + } + + protected void tickBlockEntities() { +@@ -866,11 +888,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + gameprofilerfiller.push("blockEntities"); + timings.tileEntityPending.startTiming(); // Spigot +- this.tickingBlockEntities = true; +- if (!this.pendingBlockEntityTickers.isEmpty()) { +- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); +- this.pendingBlockEntityTickers.clear(); +- } ++ final io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = ((ServerLevel)this).getCurrentWorldData(); // Paper - regionised ticking ++ regionisedWorldData.seTtickingBlockEntities(true); // Paper - regionised ticking ++ regionisedWorldData.pushPendingTickingBlockEntities(); // Paper - regionised ticking ++ List blockEntityTickers = regionisedWorldData.getBlockEntityTickers(); // Paper - regionised ticking + timings.tileEntityPending.stopTiming(); // Spigot + + timings.tileEntityTick.startTiming(); // Spigot +@@ -879,9 +900,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + int tilesThisCycle = 0; + var toRemove = new it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet(net.minecraft.Util.identityStrategy()); // Paper - use removeAll + toRemove.add(null); +- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters +- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; +- TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(tileTickPosition); ++ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Paper - regionised ticking ++ TickingBlockEntity tickingblockentity = (TickingBlockEntity) blockEntityTickers.get(i); // Paper - regionised ticking + // Spigot start + if (tickingblockentity == null) { + this.getCraftServer().getLogger().severe("Spigot has detected a null entity and has removed it, preventing a crash"); +@@ -898,19 +918,19 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } else if (this.shouldTickBlocksAt(tickingblockentity.getPos())) { + tickingblockentity.tick(); + // Paper start - execute chunk tasks during tick +- if ((this.tileTickPosition & 7) == 0) { ++ if ((i & 7) == 0) { // Paper - regionised ticking + MinecraftServer.getServer().executeMidTickTasks(); + } + // Paper end - execute chunk tasks during tick + } + } +- this.blockEntityTickers.removeAll(toRemove); ++ blockEntityTickers.removeAll(toRemove); // Paper - regionised ticking + + timings.tileEntityTick.stopTiming(); // Spigot +- this.tickingBlockEntities = false; +- co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper ++ regionisedWorldData.seTtickingBlockEntities(false); // Paper - regionised ticking ++ //co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper // Paper - region threading + gameprofilerfiller.pop(); +- spigotConfig.currentPrimedTnt = 0; // Spigot ++ regionisedWorldData.currentPrimedTnt = 0; // Spigot // Paper - region threading + } + + public void guardEntityTick(Consumer tickConsumer, T entity) { +@@ -1008,7 +1028,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { + // Paper start - Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; +- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) { ++ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(blockposition)) != null) { // Paper - region threading + return blockEntity; + } + // Paper end +@@ -1021,8 +1041,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + if (!this.isOutsideBuildHeight(blockposition)) { + // CraftBukkit start +- if (this.captureBlockStates) { +- this.capturedTileEntities.put(blockposition.immutable(), blockEntity); ++ if (this.getCurrentWorldData().captureBlockStates) { // Paper - region threading ++ this.getCurrentWorldData().capturedTileEntities.put(blockposition.immutable(), blockEntity); // Paper - region threading + return; + } + // CraftBukkit end +@@ -1226,13 +1246,30 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public void disconnect() {} + ++ @Override // Paper - region threading + public long getGameTime() { +- return this.levelData.getGameTime(); ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); + } + + public long getDayTime() { +- return this.levelData.getDayTime(); ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); ++ } ++ ++ // Paper start - region threading ++ @Override ++ public long dayTime() { ++ return this.getDayTime(); ++ } ++ ++ @Override ++ public long getRedstoneGameTime() { ++ return this.getCurrentWorldData().getRedstoneGameTime(); + } ++ // Paper end - region threading + + public boolean mayInteract(Player player, BlockPos pos) { + return true; +@@ -1438,8 +1475,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { + // Paper end +- this.randValue = this.randValue * 3 + 1013904223; +- int i1 = this.randValue >> 2; ++ int i1 = this.random.nextInt() >> 2; // Paper - region threading + + out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call + return out; // Paper +@@ -1470,7 +1506,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public long nextSubTickCount() { +- return (long) (this.subTickCount++); ++ return this.subTickCount.getAndIncrement(); // Paper - region threading + } + + public static enum ExplosionInteraction { +diff --git a/src/main/java/net/minecraft/world/level/LevelAccessor.java b/src/main/java/net/minecraft/world/level/LevelAccessor.java +index 73d1adc5ddf0363966eac0c77c8dfbbb20a2b6a3..0e2c93b55cf0c14b5f439d722c98fe9665da591f 100644 +--- a/src/main/java/net/minecraft/world/level/LevelAccessor.java ++++ b/src/main/java/net/minecraft/world/level/LevelAccessor.java +@@ -35,12 +35,22 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess { + + LevelTickAccess getBlockTicks(); + ++ // Paper start - region threading ++ default long getGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ ++ default long getRedstoneGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ // Paper end - region threading ++ + default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { // CraftBukkit - decompile error +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, priority, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, priority, this.nextSubTickCount()); // Paper - region threading + } + + default ScheduledTick createTick(BlockPos pos, T type, int delay) { // CraftBukkit - decompile error +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, this.nextSubTickCount()); // Paper - region threading + } + + default void scheduleTick(BlockPos pos, Block block, int delay, TickPriority priority) { +diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java +index 7fe1b8856bf916796fa6d2a984f0a07a2331e23b..5bedb130094c4e510f2467fbdb5c393c0972043a 100644 +--- a/src/main/java/net/minecraft/world/level/LevelReader.java ++++ b/src/main/java/net/minecraft/world/level/LevelReader.java +@@ -135,6 +135,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM + return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); + } + ++ // Paper start - region threaeding ++ default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) { ++ if (status == null || status.isOrAfter(ChunkStatus.FULL)) { ++ throw new IllegalArgumentException("Status: " + status.getName()); ++ } ++ return this.getChunk(chunkX, chunkZ, status, true); ++ } ++ // Paper end - region threaeding ++ + default ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status) { + return this.getChunk(chunkX, chunkZ, status, true); + } +@@ -204,6 +213,25 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM + return maxY >= this.getMinBuildHeight() && minY < this.getMaxBuildHeight() ? this.hasChunksAt(minX, minZ, maxX, maxZ) : false; + } + ++ // Paper start - region threading ++ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { ++ int i = SectionPos.blockToSectionCoord(minX); ++ int j = SectionPos.blockToSectionCoord(maxX); ++ int k = SectionPos.blockToSectionCoord(minZ); ++ int l = SectionPos.blockToSectionCoord(maxZ); ++ ++ for(int m = i; m <= j; ++m) { ++ for(int n = k; n <= l; ++n) { ++ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !io.papermc.paper.util.TickThread.isTickThreadFor(world, m, n))) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ // Paper end - region threading ++ + /** @deprecated */ + @Deprecated + default boolean hasChunksAt(int minX, int minZ, int maxX, int maxZ) { +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 01b21f520ef1c834b9bafc3de85c1fa4fcf539d6..593a7a4a852a4f667d6847c1120c09d1fe0f926d 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -146,7 +146,7 @@ public final class NaturalSpawner { + int limit = enumcreaturetype.getMaxInstancesPerChunk(); + SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); + if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { +- spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; ++ spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && world.getRedstoneGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Paper - region threading + limit = world.getWorld().getSpawnLimit(spawnCategory); + } + +@@ -159,20 +159,7 @@ public final class NaturalSpawner { + int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER; + int difference = k1 - currEntityCount; + +- if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { +- int minDiff = Integer.MAX_VALUE; +- final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = world.getChunkSource().chunkMap.playerMobDistanceMap.getObjectsInRange(chunk.getPos()); +- if (inRange != null) { +- final Object[] backingSet = inRange.getBackingSet(); +- for (int k = 0; k < backingSet.length; k++) { +- if (!(backingSet[k] instanceof final net.minecraft.server.level.ServerPlayer player)) { +- continue; +- } +- minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(player, enumcreaturetype), minDiff); +- } +- } +- difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; +- } ++ // Paper - region threading + if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) { + // Paper end + // CraftBukkit end +@@ -182,7 +169,7 @@ public final class NaturalSpawner { + Objects.requireNonNull(info); + // Paper start + int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, +- difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); ++ difference, false && world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); // Paper - region threading + info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum); + // Paper end + } +diff --git a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +index 3d377b9e461040405e0a7dcbd72d1506b48eb44e..ec0e143ace34c2b5990bb31ccf21dd494bdbd33d 100644 +--- a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java ++++ b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +@@ -7,6 +7,12 @@ public interface ServerLevelAccessor extends LevelAccessor { + + ServerLevel getLevel(); + ++ // Paper start - region threading ++ default public StructureManager structureManager() { ++ throw new UnsupportedOperationException(); ++ } ++ // Paper end - region threading ++ + default void addFreshEntityWithPassengers(Entity entity) { + // CraftBukkit start + this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); +diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java +index bad7031426ae6c750ae4376beb238186e7d65270..bb2d87b19838e25d9f8b5120c4ec8b9ee0f96db1 100644 +--- a/src/main/java/net/minecraft/world/level/StructureManager.java ++++ b/src/main/java/net/minecraft/world/level/StructureManager.java +@@ -44,11 +44,8 @@ public class StructureManager { + } + + public List startsForStructure(ChunkPos pos, Predicate predicate) { +- // Paper start +- return this.startsForStructure(pos, predicate, null); +- } +- public List startsForStructure(ChunkPos pos, Predicate predicate, @Nullable ServerLevelAccessor levelAccessor) { +- Map map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); ++ // Paper - region threading ++ Map map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + // Paper end + ImmutableList.Builder builder = ImmutableList.builder(); + +@@ -113,18 +110,14 @@ public class StructureManager { + } + + public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey structureTag) { +- // Paper start +- return this.getStructureWithPieceAt(pos, structureTag, null); +- } +- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey structureTag, @Nullable ServerLevelAccessor levelAccessor) { +- // Paper end ++ // Paper - region threading + Registry registry = this.registryAccess().registryOrThrow(Registries.STRUCTURE); + + for(StructureStart structureStart : this.startsForStructure(new ChunkPos(pos), (structure) -> { + return registry.getHolder(registry.getId(structure)).map((reference) -> { + return reference.is(structureTag); + }).orElse(false); +- }, levelAccessor)) { // Paper ++ })) { // Paper // Paper - region threading + if (this.structureHasPieceAt(pos, structureStart)) { + return structureStart; + } +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 7b71073027f4cf79736546500ededdfbb83d968e..49721b17af6c5f4fbdd9a2cdd1aab71ec8ced15b 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -398,8 +398,8 @@ public class Block extends BlockBehaviour implements ItemLike { + + entityitem.setDefaultPickUpDelay(); + // CraftBukkit start +- if (world.captureDrops != null) { +- world.captureDrops.add(entityitem); ++ if (world.getCurrentWorldData().captureDrops != null) { // Paper - region threading ++ world.getCurrentWorldData().captureDrops.add(entityitem); // Paper - region threading + } else { + world.addFreshEntity(entityitem); + } +diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java +index 03fde6e47c4a347c62fe9b4a3351769aedf874f6..d43e111d63ac3432e51a5f31ae3eb66246893156 100644 +--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java +@@ -24,7 +24,7 @@ public class BushBlock extends Block { + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { + // CraftBukkit start + if (!state.canSurvive(world, pos)) { +- if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper ++ if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Paper - region threading + return Blocks.AIR.defaultBlockState(); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +index 1ec242205b82a5a1f10deb2312795cc5dc157a76..e1d17e63c5ba43a69d48748a66275de3498a18a3 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -118,9 +118,9 @@ public class CactusBlock extends Block { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Paper - region threading + entity.hurt(DamageSource.CACTUS, 1.0F); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Paper - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +index a4c44cb59dee29cf227dbb51bfc1576d89dfb2e3..8683451bcd1ef207663c187a02304e6b0e97f274 100644 +--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +@@ -95,9 +95,9 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper + if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Paper - region threading + entity.hurt(DamageSource.IN_FIRE, (float) this.fireDamage); +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Paper - region threading + } + + super.entityInside(state, world, pos, entity); +diff --git a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +index 16504b8be08064e61b013fa943f692816612cbd0..23f340c8b8f1d00b183bdacabf0bde3124886fda 100644 +--- a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +@@ -113,7 +113,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { + } + + private static void tickEntity(Level world, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { +- if (world.getGameTime() % 20L == 0L) { ++ if (world.getRedstoneGameTime() % 20L == 0L) { // Paper - region threading + DaylightDetectorBlock.updateSignalStrength(state, world, pos); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +index 8f55d0753fa26924235c943595f0d1a06a933a6f..970ec44a19163bf2c22567c29dc4a7e456d8a202 100644 +--- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +@@ -47,7 +47,7 @@ public class DispenserBlock extends BaseEntityBlock { + object2objectopenhashmap.defaultReturnValue(new DefaultDispenseItemBehavior()); + }); + private static final int TRIGGER_DURATION = 4; +- public static boolean eventFired = false; // CraftBukkit ++ public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Paper - region threading + + public static void registerBehavior(ItemLike provider, DispenseItemBehavior behavior) { + DispenserBlock.DISPENSER_REGISTRY.put(provider.asItem(), behavior); +@@ -94,7 +94,7 @@ public class DispenserBlock extends BaseEntityBlock { + + if (idispensebehavior != DispenseItemBehavior.NOOP) { + if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - BlockPreDispenseEvent is called here +- DispenserBlock.eventFired = false; // CraftBukkit - reset event status ++ DispenserBlock.eventFired.set(false); // CraftBukkit - reset event status // Paper - region threading + tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack)); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +index e234ae13fe9793db237adb6f6216fa32638cfc4f..6d0ed8e7003c402c246ca6834721ac7e4623a3ae 100644 +--- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +@@ -95,7 +95,7 @@ public class DoublePlantBlock extends BushBlock { + + protected static void preventCreativeDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) { + // CraftBukkit start +- if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper ++ if (((net.minecraft.server.level.ServerLevel)world).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Paper - region threading + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +index 683f24251baf8ef3bef8f32ba83dc7f0e8ed7d70..f138f30c3e6588708811cd0d511fc66c9a334ed9 100644 +--- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +@@ -81,7 +81,7 @@ public class HoneyBlock extends HalfTransparentBlock { + } + + private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { +- if (entity instanceof ServerPlayer && entity.level.getGameTime() % 20L == 0L) { ++ if (entity instanceof ServerPlayer && entity.level.getRedstoneGameTime() % 20L == 0L) { // Paper - region threading + CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level.getBlockState(pos)); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +index da3b301a42a93c891d083a6e02d1be8ed35adf1d..4bd815ba372809fb2a4b70bf04ac836750f25609 100644 +--- a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +@@ -112,7 +112,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc + + @Override + public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { +- if (world.isThundering() && (long) world.random.nextInt(200) <= world.getGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { ++ if (world.isThundering() && (long) world.random.nextInt(200) <= world.getRedstoneGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { // Paper - region threading + ParticleUtils.spawnParticlesAlongAxis(((Direction) state.getValue(LightningRodBlock.FACING)).getAxis(), world, pos, 0.125D, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +index d3540a4daaa8021ae009bfd4d9ef4f1172ab4c56..70d046e0fe0de64691195bd5fde36b3eb1f5e968 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -29,9 +29,9 @@ public class MagmaBlock extends Block { + @Override + public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { + if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Paper - region threading + entity.hurt(DamageSource.HOT_FLOOR, 1.0F); +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Paper - region threading + } + + super.stepOn(world, pos, state, entity); +diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +index e78fdd317d59cfca6a28deb6e0bd02aabe91e930..9c61ef8643ddfef3aeae0df597e309b94daff4b9 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -143,9 +143,9 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { + if (state.getValue(PointedDripstoneBlock.TIP_DIRECTION) == Direction.UP && state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP) { +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Paper - region threading + entity.causeFallDamage(fallDistance + 2.0F, 2.0F, DamageSource.STALAGMITE); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Paper - region threading + } else { + super.fallOn(world, state, pos, entity, fallDistance); + } +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 5ea09cc455bd86beb450f0e0275d7c6c8da98084..551666836298d98b062a46afee24755a08b9b86c 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -262,7 +262,7 @@ public class RedStoneWireBlock extends Block { + * Note: Added 'source' argument so as to help determine direction of information flow + */ + private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) { +- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { // Paper - region threading + turbo.updateSurroundingRedstone(worldIn, pos, state, source); + return; + } +@@ -286,7 +286,7 @@ public class RedStoneWireBlock extends Block { + int k = worldIn.getBestNeighborSignal(pos1); + this.shouldSignal = true; + +- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Paper - region threading + // This code is totally redundant to if statements just below the loop. + if (k > 0 && k > j - 1) { + j = k; +@@ -300,7 +300,7 @@ public class RedStoneWireBlock extends Block { + // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the + // following loop can affect the power level of the wire. Therefore, the loop is + // skipped if k is already 15. +- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { // Paper - region threading + for (Direction enumfacing : Direction.Plane.HORIZONTAL) { + BlockPos blockpos = pos1.relative(enumfacing); + boolean flag = blockpos.getX() != pos2.getX() || blockpos.getZ() != pos2.getZ(); +@@ -319,7 +319,7 @@ public class RedStoneWireBlock extends Block { + } + } + +- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Paper - region threading + // The old code would decrement the wire value only by 1 at a time. + if (l > j) { + j = l - 1; +@@ -455,7 +455,7 @@ public class RedStoneWireBlock extends Block { + public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { + if (!oldState.is(state.getBlock()) && !world.isClientSide) { + // Paper start - optimize redstone - replace call to updatePowerStrength +- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Paper - region threading + world.getWireHandler().onWireAdded(pos); // Alternate Current + } else { + this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft +@@ -488,7 +488,7 @@ public class RedStoneWireBlock extends Block { + } + + // Paper start - optimize redstone - replace call to updatePowerStrength +- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Paper - region threading + world.getWireHandler().onWireRemoved(pos, state); // Alternate Current + } else { + this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft +@@ -529,7 +529,7 @@ public class RedStoneWireBlock extends Block { + if (!world.isClientSide) { + // Paper start - optimize redstone (Alternate Current) + // Alternate Current handles breaking of redstone wires in the WireHandler. +- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Paper - region threading + world.getWireHandler().onWireUpdated(pos); + } else + // Paper end +diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +index da07fce0cf7c9fbdb57d2c59e431b59bf583bf50..d31ab3f657369688ddd593ad04ed9cb705319b0e 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +@@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends TorchBlock { + public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { + boolean flag = this.hasNeighborSignal(world, pos, state); + // Paper start +- java.util.ArrayDeque redstoneUpdateInfos = world.redstoneUpdateInfos; ++ java.util.ArrayDeque redstoneUpdateInfos = world.getCurrentWorldData().redstoneUpdateInfos; // Paper - region threading + if (redstoneUpdateInfos != null) { + RedstoneTorchBlock.Toggle curr; +- while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) { ++ while ((curr = redstoneUpdateInfos.peek()) != null && world.getRedstoneGameTime() - curr.when > 60L) { // Paper - region threading + redstoneUpdateInfos.poll(); + } + } +@@ -157,14 +157,14 @@ public class RedstoneTorchBlock extends TorchBlock { + + private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) { + // Paper start +- java.util.ArrayDeque list = world.redstoneUpdateInfos; ++ java.util.ArrayDeque list = world.getCurrentWorldData().redstoneUpdateInfos; // Paper - region threading + if (list == null) { +- list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>(); ++ list = world.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Paper - region threading + } + + + if (addNew) { +- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime())); ++ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getRedstoneGameTime())); // Paper - region threading + } + + int i = 0; +@@ -185,12 +185,18 @@ public class RedstoneTorchBlock extends TorchBlock { + + public static class Toggle { + +- final BlockPos pos; +- final long when; ++ public final BlockPos pos; // Paper - region threading ++ long when; // Paper - region ticking + + public Toggle(BlockPos pos, long time) { + this.pos = pos; + this.when = time; + } ++ ++ // Paper start - region ticking ++ public void offsetTime(long offset) { ++ this.when += offset; ++ } ++ // Paper end - region ticking + } + } +diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +index 901978a338f0f1b6f20ffb65aac59704bfa6f36a..63b09042a603abb9c8adf651061ebf972725c6dd 100644 +--- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +@@ -52,18 +52,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { + world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4); + } else { + // CraftBukkit start +- if (world.captureTreeGeneration) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ if (worldData.captureTreeGeneration) { // Paper - region threading + this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + } else { +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Paper - region threading + this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); +- world.captureTreeGeneration = false; +- if (world.capturedBlockStates.size() > 0) { ++ worldData.captureTreeGeneration = false; // Paper - region threading ++ if (worldData.capturedBlockStates.size() > 0) { // Paper - region threading + TreeType treeType = SaplingBlock.treeType; + SaplingBlock.treeType = null; + Location location = new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); +- java.util.List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + StructureGrowEvent event = null; + if (treeType != null) { + event = new StructureGrowEvent(location, treeType, false, null, blocks); +diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +index af46c05a34292d271fd4a809398e6b299e10b12b..420a4cef902edf25cf4722b195be43d29c5a6d92 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +@@ -51,7 +51,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + + @Override + public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { +- if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper ++ if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper // Paper - regionised ticking + // Paper start + net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); + if (cachedBlockChunk == null) { // Is this needed? +diff --git a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +index c926cd3ebb916115a608e86b389ffe7e15d48cd7..9985714f2ee95e57256eeef7d7fb6a54aa331c98 100644 +--- a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +@@ -86,9 +86,9 @@ public class SweetBerryBushBlock extends BushBlock implements BonemealableBlock + double d1 = Math.abs(entity.getZ() - entity.zOld); + + if (d0 >= 0.003000000026077032D || d1 >= 0.003000000026077032D) { +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Paper - region threading + entity.hurt(DamageSource.SWEET_BERRY_BUSH, 1.0F); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Paper - region threading + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +index b91effe91dad2e1aeea0ea31140f7432833b343f..e41ad437d61e04bfe6430911a0d85afca4375bc5 100644 +--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -53,7 +53,7 @@ public class WitherSkullBlock extends SkullBlock { + } + + public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) { +- if (world.captureBlockStates) return; // CraftBukkit ++ if (world.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Paper - region threading + if (!world.isClientSide) { + BlockState iblockdata = blockEntity.getBlockState(); + boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +index 928625b5ab054ffa412be8a438f58291cc7a3cc0..5560e0a83662872627fdad78d96a9c7a2b51de75 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -202,7 +202,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name + } + + i1 = blockEntity.levels; +- if (world.getGameTime() % 80L == 0L) { ++ if (world.getRedstoneGameTime() % 80L == 0L) { // Paper - region threading + if (!blockEntity.beamSections.isEmpty()) { + blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BellBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BellBlockEntity.java +index 648d8f3e72e30aacf68eb073a1ac30f8ec29503c..b15ba5dd5e27c53652f965893344d48df395ddb8 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BellBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BellBlockEntity.java +@@ -36,6 +36,18 @@ public class BellBlockEntity extends BlockEntity { + private boolean resonating; + private int resonationTicks; + ++ // Paper start - region ticking ++ ++ @Override ++ public void updateTicks(long fromTickOffset, long fromGameTimeOffset) { ++ super.updateTicks(fromTickOffset, fromGameTimeOffset); ++ if (this.lastRingTimestamp != 0L) { ++ this.lastRingTimestamp += fromGameTimeOffset; ++ } ++ } ++ ++ // Paper end - region ticking ++ + public BellBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.BELL, pos, state); + } +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +index 58986bc0677c5ea1ad54d7d6d4efa5c2ea233aea..49f83e421642a00ed5025c851ac73261cfd971f4 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -42,6 +42,12 @@ public abstract class BlockEntity { + protected boolean remove; + private BlockState blockState; + ++ // Paper start - region ticking ++ public void updateTicks(final long fromTickOffset, final long fromGameTimeOffset) { ++ ++ } ++ // Paper end - region ticking ++ + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + this.type = type; + this.worldPosition = pos.immutable(); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +index 55006724ccec9f3de828ec18693728e9741ff65f..751b1d8f67821dc07be8228cdd65f065769eed01 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +@@ -54,7 +54,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + public int fuel; + protected final ContainerData dataAccess; + // CraftBukkit start - add fields and methods +- private int lastTick = MinecraftServer.currentTick; ++ //private int lastTick = MinecraftServer.currentTick; // Paper - region ticking - restore original timers + public List transaction = new java.util.ArrayList(); + private int maxStack = 64; + +@@ -171,11 +171,10 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + ItemStack itemstack1 = (ItemStack) blockEntity.items.get(3); + + // CraftBukkit start - Use wall time instead of ticks for brewing +- int elapsedTicks = MinecraftServer.currentTick - blockEntity.lastTick; +- blockEntity.lastTick = MinecraftServer.currentTick; ++ // Paper - region ticking - restore original timers + + if (flag1) { +- blockEntity.brewTime -= elapsedTicks; ++ --blockEntity.brewTime; // Paper - region ticking - restore original timers + boolean flag2 = blockEntity.brewTime <= 0; // == -> <= + // CraftBukkit end + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index 05eab04e4aec4151018f25b59f92ddbbb4c09f87..97f20df314d4f8e956a53c767c52ae6abd41e645 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -52,6 +52,16 @@ public class ConduitBlockEntity extends BlockEntity { + private UUID destroyTargetUUID; + private long nextAmbientSoundActivation; + ++ // Paper start - region ticking ++ @Override ++ public void updateTicks(long fromTickOffset, long fromGameTimeOffset) { ++ super.updateTicks(fromTickOffset, fromGameTimeOffset); ++ if (this.nextAmbientSoundActivation != 0L) { ++ this.nextAmbientSoundActivation += fromGameTimeOffset; ++ } ++ } ++ // Paper end - region ticking ++ + public ConduitBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.CONDUIT, pos, state); + } +@@ -88,7 +98,7 @@ public class ConduitBlockEntity extends BlockEntity { + + public static void clientTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + ++blockEntity.tickCount; +- long i = world.getGameTime(); ++ long i = world.getRedstoneGameTime(); // Paper - region threading + List list = blockEntity.effectBlocks; + + if (i % 40L == 0L) { +@@ -106,7 +116,7 @@ public class ConduitBlockEntity extends BlockEntity { + + public static void serverTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + ++blockEntity.tickCount; +- long i = world.getGameTime(); ++ long i = world.getRedstoneGameTime(); // Paper - region threading + List list = blockEntity.effectBlocks; + + if (i % 40L == 0L) { +@@ -236,11 +246,11 @@ public class ConduitBlockEntity extends BlockEntity { + + if (blockEntity.destroyTarget != null) { + // CraftBukkit start +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // Paper - region threading + if (blockEntity.destroyTarget.hurt(DamageSource.MAGIC, 4.0F)) { + world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); + } +- CraftEventFactory.blockDamage = null; ++ CraftEventFactory.blockDamageRT.set(null); // Paper - region threading + // CraftBukkit end + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index ccad692aba2ed77259f6814d88f01b91ed9d229b..1e2b1658d2eee5a44bdde736a3449567daf53171 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -77,6 +77,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + // CraftBukkit end + ++ // Paper start - region ticking ++ @Override ++ public void updateTicks(long fromTickOffset, long fromGameTimeOffset) { ++ super.updateTicks(fromTickOffset, fromGameTimeOffset); ++ if (this.tickedGameTime != 0L) { ++ this.tickedGameTime += fromGameTimeOffset; ++ } ++ } ++ // Paper end - region ticking ++ + public HopperBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.HOPPER, pos, state); + this.items = NonNullList.withSize(5, ItemStack.EMPTY); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 163e63e3c538c7c1c75ed634896db9d8c00416f3..766034bf1f5c10106ed728fc3b8164e3ea2f38b7 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -86,9 +86,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + } + + public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Paper - region threading + blockEntity.sculkSpreader.updateCursors(world, pos, world.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Paper - region threading + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index f80545f80948db27d1fbde77d0505c916eb504ed..3ed3a9c1c441e1ae400ca4d852f36bae722d6039 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -51,9 +51,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + public long age; + private int teleportCooldown; + @Nullable +- public BlockPos exitPortal; ++ public volatile BlockPos exitPortal; // Paper - region threading - volatile + public boolean exactTeleport; + ++ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Paper - region threading ++ private Long searchingForExitId; // Paper - region threading ++ + public TheEndGatewayBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.END_GATEWAY, pos, state); + } +@@ -128,7 +131,7 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + } + + public static boolean canEntityTeleport(Entity entity) { +- return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown(); ++ return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown() && entity.canPortalAsync(true); // Paper - region threading - correct portal check + } + + public boolean isSpawning() { +@@ -176,8 +179,112 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + } + } + ++ // Paper start - region threading ++ private void trySearchForExit(ServerLevel world, BlockPos fromPos) { ++ if (this.searchingForExitId != null) { ++ return; ++ } ++ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); ++ int chunkX = fromPos.getX() >> 4; ++ int chunkZ = fromPos.getZ() >> 4; ++ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ ++ ca.spottedleaf.concurrentutil.completable.Completable complete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ ++ complete.addWaiter((tpLoc, throwable) -> { ++ // create the exit portal ++ TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); ++ TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); ++ ++ // need to go onto the tick thread to avoid saving issues ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // update the exit portal location ++ TheEndGatewayBlockEntity.this.exitPortal = tpLoc; ++ ++ // remove ticket keeping the gateway loaded ++ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ TheEndGatewayBlockEntity.this.searchingForExitId = null; ++ } ++ ); ++ }); ++ ++ findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); ++ } ++ ++ private static void teleportRegionThreading(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { ++ // can we even teleport in this dimension? ++ if (blockEntity.exitPortal == null && world.getTypeKey() != LevelStem.END) { ++ return; ++ } ++ ++ ServerLevel serverWorld = (ServerLevel)world; ++ ++ // First, find the position we are trying to teleport to ++ BlockPos teleportPos = blockEntity.exitPortal; ++ boolean isExactTeleport = blockEntity.exactTeleport; ++ ++ if (teleportPos == null) { ++ blockEntity.trySearchForExit(serverWorld, pos); ++ return; ++ } ++ ++ // This needs to be first, as we are only guaranteed to be on the corresponding region tick thread here ++ TheEndGatewayBlockEntity.triggerCooldown(world, pos, state, blockEntity); ++ ++ if (isExactTeleport) { ++ // blind teleport ++ entity.teleportAsync( ++ serverWorld, Vec3.atCenterOf(teleportPos), null, null, null, ++ PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ (Entity teleportedEntity) -> { ++ for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) { ++ passenger.setPortalCooldown(); ++ } ++ } ++ ); ++ } else { ++ // we could hack around by first loading the chunks, then calling back to here and checking if the entity ++ // should be teleported, something something else... ++ // however, we know the target location cannot differ by one region section: so we can ++ // just teleport and adjust the position after ++ entity.teleportAsync( ++ serverWorld, Vec3.atCenterOf(teleportPos), null, null, null, ++ PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ (Entity teleportedEntity) -> { ++ for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) { ++ passenger.setPortalCooldown(); ++ } ++ ++ // adjust to the final exit position ++ Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(serverWorld, teleportPos)); ++ // teleportTo will adjust rider positions ++ teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); ++ } ++ ); ++ } ++ } ++ // Paper end - region threading ++ + public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { + if (world instanceof ServerLevel && !blockEntity.isCoolingDown()) { ++ // Paper start - region threading ++ if (true) { ++ teleportRegionThreading(world, pos, state, entity.getRootVehicle(), blockEntity); ++ return; ++ } ++ // Paper end - region threading + ServerLevel worldserver = (ServerLevel) world; + + blockEntity.teleportCooldown = 100; +@@ -281,6 +388,125 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + return TheEndGatewayBlockEntity.findTallestBlock(world, blockposition1, 16, true); + } + ++ // Paper start - region threading ++ private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.Completable complete) { ++ ca.spottedleaf.concurrentutil.completable.Completable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.Completable<>(); ++ ++ tentativeSelection.addWaiter((vec3d, throwable) -> { ++ LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); ++ BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); ++ if (blockposition1 == null) { ++ BlockPos blockposition2 = new BlockPos(vec3d.x + 0.5D, 75.0D, vec3d.z + 0.5D); ++ ++ TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); ++ world.registryAccess().registry(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { ++ return iregistry.getHolder(EndFeatures.END_ISLAND); ++ }).ifPresent((holder_c) -> { ++ ((ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); ++ }); ++ blockposition1 = blockposition2; ++ } else { ++ TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); ++ } ++ ++ // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained ++ // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue ++ // (regioniser only guarantees at least 8 chunks along a single axis) ++ // so, we need to schedule for the next tick ++ int posX = blockposition1.getX(); ++ int posZ = blockposition1.getZ(); ++ int radius = 16; ++ ++ BlockPos finalBlockPosition1 = blockposition1; ++ world.loadChunksAsync(blockposition1, radius, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (List chunks) -> { ++ // make sure chunks are kept loaded ++ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { ++ world.chunkSource.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.DELAYED, access.getPos(), ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ } ++ // now after the chunks are loaded, we can delay by one tick ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, posX >> 4, posZ >> 4, () -> { ++ // find final location ++ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true); ++ ++ // done ++ complete.complete(tpLoc.above(10)); ++ } ++ ); ++ } ++ ); ++ }); ++ ++ // fire off chain ++ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); ++ } ++ ++ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.Completable complete) { ++ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); ++ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); ++ ++ class Vars { ++ int i = 16; ++ boolean mode = false; ++ Vec3 currPos = posDirExtruded; ++ } ++ Vars vars = new Vars(); ++ ++ Runnable handle = new Runnable() { ++ @Override ++ public void run() { ++ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { ++ vars.i = 0; // fall back to completing ++ } ++ ++ // try to load next chunk ++ if (vars.i-- <= 0) { ++ if (vars.mode) { ++ complete.complete(vars.currPos); ++ return; ++ } ++ vars.mode = true; ++ vars.i = 16; ++ } ++ ++ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); ++ // schedule next iteration ++ Runnable handleButInitialised = this; ++ world.chunkTaskScheduler.scheduleChunkLoad( ++ io.papermc.paper.util.CoordinateUtils.getChunkX(vars.currPos), ++ io.papermc.paper.util.CoordinateUtils.getChunkZ(vars.currPos), ++ net.minecraft.world.level.chunk.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (chunk) -> { ++ handleButInitialised.run(); ++ } ++ ); ++ } ++ }; ++ ++ // kick off first chunk load ++ world.chunkTaskScheduler.scheduleChunkLoad( ++ io.papermc.paper.util.CoordinateUtils.getChunkX(posDirExtruded), ++ io.papermc.paper.util.CoordinateUtils.getChunkZ(posDirExtruded), ++ net.minecraft.world.level.chunk.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ (chunk) -> { ++ handle.run(); ++ } ++ ); ++ } ++ // Paper end - region threading ++ + private static Vec3 findExitPortalXZPosTentative(ServerLevel world, BlockPos pos) { + Vec3 vec3d = (new Vec3((double) pos.getX(), 0.0D, (double) pos.getZ())).normalize(); + boolean flag = true; +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +index 221c5d080d55326e458c1182823d6b49224ef498..de2f6d0271bb55f2fe0a5627e6cdc5879584456d 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +@@ -44,6 +44,16 @@ public class PistonMovingBlockEntity extends BlockEntity { + private long lastTicked; + private int deathTicks; + ++ // Paper start - region ticking ++ @Override ++ public void updateTicks(long fromTickOffset, long fromGameTimeOffset) { ++ super.updateTicks(fromTickOffset, fromGameTimeOffset); ++ if (this.lastTicked != 0L) { ++ this.lastTicked += fromGameTimeOffset; ++ } ++ } ++ // Paper end - region ticking ++ + public PistonMovingBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityType.PISTON, pos, state); + } +@@ -144,8 +154,8 @@ public class PistonMovingBlockEntity extends BlockEntity { + + entity.setDeltaMovement(e, g, h); + // Paper - EAR items stuck in in slime pushed by a piston +- entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); +- entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); ++ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 10); // Paper - region threading ++ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 10); // Paper - region threading + // Paper end + break; + } +diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java +index 7a12a4da4864306ec6589ca81368e84718825047..75fef911238058a5616a2504502de5b911563f06 100644 +--- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java ++++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java +@@ -33,19 +33,19 @@ public class WorldBorder { + + public WorldBorder() {} + ++ // Paper - region threading - TODO make this shit thread-safe ++ + public boolean isWithinBounds(BlockPos pos) { + return (double) (pos.getX() + 1) > this.getMinX() && (double) pos.getX() < this.getMaxX() && (double) (pos.getZ() + 1) > this.getMinZ() && (double) pos.getZ() < this.getMaxZ(); + } + + // Paper start +- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); ++ private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Paper - region threading + public boolean isBlockInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(chunkX, 64, chunkZ); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Paper - region threading + } + public boolean isChunkInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Paper - region threading + } + // Paper end + +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index 7e9c388179c75a233d9b179ea1e00428ac65ee99..90c91179c189f9ed9bd90587978f02fc1853866e 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -308,7 +308,7 @@ public abstract class ChunkGenerator { + return Pair.of(placement.getLocatePos(pos), holder); + } + +- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); ++ ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - region threading + + structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess); + } while (structurestart == null); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index e776eb8afef978938da084f9ae29d611181b43fe..928351ab809c0be2366ebaab1d1b653ce214e221 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -233,51 +233,15 @@ public class LevelChunk extends ChunkAccess { + } + // Paper end + // Paper start - optimise checkDespawn +- private boolean playerGeneralAreaCacheSet; +- private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; +- +- public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayerGeneralAreaCache() { +- if (!this.playerGeneralAreaCacheSet) { +- this.updateGeneralAreaCache(); +- } +- return this.playerGeneralAreaCache; +- } +- +- public void updateGeneralAreaCache() { +- this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); +- } +- +- public void removeGeneralAreaCache() { +- this.playerGeneralAreaCacheSet = false; +- this.playerGeneralAreaCache = null; +- } +- +- public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { +- this.playerGeneralAreaCacheSet = true; +- this.playerGeneralAreaCache = value; +- } +- ++ // Paper - region threading + public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, + double maxRange, java.util.function.Predicate predicate) { +- if (!this.playerGeneralAreaCacheSet) { +- this.updateGeneralAreaCache(); +- } +- +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; +- +- if (nearby == null) { +- return null; +- } +- +- Object[] backingSet = nearby.getBackingSet(); ++ // Paper start - region threading + double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + net.minecraft.server.level.ServerPlayer closest = null; +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- Object _player = backingSet[i]; +- if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { +- continue; +- } +- net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; ++ java.util.List nearby = this.level.getLocalPlayers(); ++ for (int i = 0, len = nearby.size(); i < len; ++i) { ++ net.minecraft.server.level.ServerPlayer player = nearby.get(i); + + double distance = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distance < closestDistance && predicate.test(player)) { +@@ -285,31 +249,17 @@ public class LevelChunk extends ChunkAccess { + closestDistance = distance; + } + } +- + return closest; ++ // Paper end - region threading + } + + public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate predicate, + double range, java.util.List ret) { +- if (!this.playerGeneralAreaCacheSet) { +- this.updateGeneralAreaCache(); +- } +- +- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; +- +- if (nearby == null) { +- return; +- } +- ++ // Paper start - region threading + double rangeSquared = range * range; +- +- Object[] backingSet = nearby.getBackingSet(); +- for (int i = 0, len = backingSet.length; i < len; ++i) { +- Object _player = backingSet[i]; +- if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { +- continue; +- } +- net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; ++ java.util.List nearby = this.level.getLocalPlayers(); ++ for (int i = 0, len = nearby.size(); i < len; ++i) { ++ net.minecraft.server.level.ServerPlayer player = nearby.get(i); + + if (range >= 0.0) { + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); +@@ -322,6 +272,7 @@ public class LevelChunk extends ChunkAccess { + ret.add(player); + } + } ++ // Paper end - region threading + } + // Paper end - optimise checkDespawn + +@@ -557,7 +508,7 @@ public class LevelChunk extends ChunkAccess { + return null; + } else { + // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. +- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { ++ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Paper - region threading + iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); + } + +@@ -604,7 +555,7 @@ public class LevelChunk extends ChunkAccess { + @Nullable + public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { + // CraftBukkit start +- BlockEntity tileentity = level.capturedTileEntities.get(pos); ++ BlockEntity tileentity = level.getCurrentWorldData().capturedTileEntities.get(pos); // Paper - region threading + if (tileentity == null) { + tileentity = (BlockEntity) this.blockEntities.get(pos); + } +@@ -889,13 +840,13 @@ public class LevelChunk extends ChunkAccess { + + org.bukkit.World world = this.level.getWorld(); + if (world != null) { +- this.level.populating = true; ++ this.level.getCurrentWorldData().populating = true; // Paper - region threading + try { + for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { + populator.populate(world, random, bukkitChunk); + } + } finally { +- this.level.populating = false; ++ this.level.getCurrentWorldData().populating = false; // Paper - region threading + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(this.bukkitChunk)); +@@ -944,7 +895,7 @@ public class LevelChunk extends ChunkAccess { + @Override + public boolean isUnsaved() { + // Paper start - add dirty system to tick lists +- long gameTime = this.level.getLevelData().getGameTime(); ++ long gameTime = this.level.getRedstoneGameTime(); // Paper - region threading + if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) { + return true; + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index 256642f2e2aa66f7e8c00cae91a75060a8817c9c..4737011de22b17ad8c291a8cb04d71ee2f8943db 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -687,7 +687,7 @@ public class ChunkSerializer { + } + + private static void saveTicks(ServerLevel world, CompoundTag nbt, ChunkAccess.TicksToSave tickSchedulers) { +- long i = world.getLevelData().getGameTime(); ++ long i = world.getRedstoneGameTime(); // Paper - region threading + + nbt.put("block_ticks", tickSchedulers.blocks().save(i, (block) -> { + return BuiltInRegistries.BLOCK.getKey(block).toString(); +diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +index e9eb32469a5c03f7a3677ef50fd4541c1ed662ad..f09c5d7b3230d6553e825101fba164a98fa6b43b 100644 +--- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java ++++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +@@ -168,6 +168,7 @@ public class EndDragonFight { + if (!this.dragonEvent.getPlayers().isEmpty()) { + this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); + boolean bl = this.isArenaLoaded(); ++ if (!bl) { return; }// Paper - region threading - don't tick if we don't own the entire region + if (this.needsStateScanning && bl) { + this.scanState(); + this.needsStateScanning = false; +@@ -214,6 +215,12 @@ public class EndDragonFight { + } + + List list = this.level.getDragons(); ++ // Paper start - region threading ++ // we do not want to deal with any dragons NOT nearby ++ list.removeIf((dragon) -> { ++ return !io.papermc.paper.util.TickThread.isTickThreadFor(dragon); ++ }); ++ // Paper end - region threading + if (list.isEmpty()) { + this.dragonKilled = true; + } else { +@@ -324,8 +331,8 @@ public class EndDragonFight { + private boolean isArenaLoaded() { + for(int i = -8; i <= 8; ++i) { + for(int j = 8; j <= 8; ++j) { +- ChunkAccess chunkAccess = this.level.getChunk(i, j, ChunkStatus.FULL, false); +- if (!(chunkAccess instanceof LevelChunk)) { ++ ChunkAccess chunkAccess = this.level.getChunkIfLoaded(i, j); // Paper - region threading ++ if (!(chunkAccess instanceof LevelChunk) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Paper - region threading + return false; + } + +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +index a908652f1ebb426d265ef614746f70cd1e538268..08e35b6aea5f6bdfb3e5a6ef602aaeb4a97a57be 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +@@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState; + + public class PatrolSpawner implements CustomSpawner { + +- private int nextTick; ++ //private int nextTick; // Paper - region threading + + public PatrolSpawner() {} + +@@ -32,15 +32,16 @@ public class PatrolSpawner implements CustomSpawner { + return 0; + } else { + RandomSource randomsource = world.random; ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading + + // Paper start - Patrol settings + // Random player selection moved up for per player spawning and configuration +- int j = world.players().size(); ++ int j = world.getLocalPlayers().size(); // Paper - region threading + if (j < 1) { + return 0; + } + +- net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j)); ++ net.minecraft.server.level.ServerPlayer entityhuman = world.getLocalPlayers().get(randomsource.nextInt(j)); // Paper - region threading + if (entityhuman.isSpectator()) { + return 0; + } +@@ -50,8 +51,8 @@ public class PatrolSpawner implements CustomSpawner { + --entityhuman.patrolSpawnDelay; + patrolSpawnDelay = entityhuman.patrolSpawnDelay; + } else { +- this.nextTick--; +- patrolSpawnDelay = this.nextTick; ++ worldData.patrolSpawnerNextTick--; // Paper - region threading ++ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Paper - region threading + } + + if (patrolSpawnDelay > 0) { +@@ -66,7 +67,7 @@ public class PatrolSpawner implements CustomSpawner { + if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { + entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); + } else { +- this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); ++ worldData.patrolSpawnerNextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); // Paper - region threading + } + + if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 1c3718d9244513d9fc795dceb564a81375734557..d20c539d0d4f9e7fee42333ffe45cc01e05e9a43 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -24,7 +24,7 @@ import net.minecraft.world.level.material.FluidState; + + public class PhantomSpawner implements CustomSpawner { + +- private int nextTick; ++ //private int nextTick; // Paper - region threading + + public PhantomSpawner() {} + +@@ -42,20 +42,22 @@ public class PhantomSpawner implements CustomSpawner { + // Paper end + RandomSource randomsource = world.random; + +- --this.nextTick; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ ++ --worldData.phantomSpawnerNextTick; // Paper - region threading ++ if (worldData.phantomSpawnerNextTick > 0) { // Paper - region threading + return 0; + } else { + // Paper start + int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; + int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; +- this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; ++ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Paper - region threading + // Paper end + if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { + return 0; + } else { + int i = 0; +- Iterator iterator = world.players().iterator(); ++ Iterator iterator = world.getLocalPlayers().iterator(); // Paper - region threading + + while (iterator.hasNext()) { + Player entityhuman = (Player) iterator.next(); +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +index 4761aa772bc34dd66547dd4dd561c2e04c3229ad..a2a0926f2712bfb8e92b646a2105cd490ac8ded2 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +@@ -70,6 +70,7 @@ public class StructureCheck { + } + + public StructureCheckResult checkStart(ChunkPos pos, Structure type, boolean skipReferencedStructures) { ++ if (true) return StructureCheckResult.CHUNK_LOAD_NEEDED; // Paper - region threading + long l = pos.toLong(); + Object2IntMap object2IntMap = this.loadedChunks.get(l); + if (object2IntMap != null) { +@@ -95,6 +96,7 @@ public class StructureCheck { + + @Nullable + private StructureCheckResult tryLoadFromStorage(ChunkPos pos, Structure structure, boolean skipReferencedStructures, long posLong) { ++ if (true) return StructureCheckResult.CHUNK_LOAD_NEEDED; // Paper - region threading + CollectFields collectFields = new CollectFields(new FieldSelector(IntTag.TYPE, "DataVersion"), new FieldSelector("Level", "Structures", CompoundTag.TYPE, "Starts"), new FieldSelector("structures", CompoundTag.TYPE, "starts")); + + try { +@@ -182,6 +184,7 @@ public class StructureCheck { + } + + public void onStructureLoad(ChunkPos pos, Map structureStarts) { ++ if (true) return; // Paper - region threading + long l = pos.toLong(); + Object2IntMap object2IntMap = new Object2IntOpenHashMap<>(); + structureStarts.forEach((start, structureStart) -> { +@@ -194,6 +197,7 @@ public class StructureCheck { + } + + private void storeFullResults(long pos, Object2IntMap referencesByStructure) { ++ if (true) return; // Paper - region threading + this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure)); + this.featureChecks.values().forEach((generationPossibilityByChunkPos) -> { + generationPossibilityByChunkPos.remove(pos); +@@ -201,6 +205,7 @@ public class StructureCheck { + } + + public void incrementReference(ChunkPos pos, Structure structure) { ++ if (true) return; // Paper - region threading + this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> { + if (referencesByStructure == null || referencesByStructure.isEmpty()) { + referencesByStructure = new Object2IntOpenHashMap<>(); +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index 92d13c9f1ec1e5ff72c1d68f924a8d1c86c91565..8c4fb6c24437edc1daf81f2e20d22e34ef683bf1 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -89,10 +89,10 @@ public class PortalForcer { + BlockPos blockposition1 = villageplacerecord.getPos(); + + this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockposition1), 3, blockposition1); +- BlockState iblockdata = this.level.getBlockState(blockposition1); ++ BlockState iblockdata = this.level.getBlockStateFromEmptyChunk(blockposition1); // Paper - region threading + + return BlockUtil.getLargestRectangleAround(blockposition1, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition2) -> { +- return this.level.getBlockState(blockposition2) == iblockdata; ++ return this.level.getBlockStateFromEmptyChunk(blockposition2) == iblockdata; // Paper - region threading + }); + }); + } +diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +index b1c594dc6a6b8a6c737b99272acab9e7dbd0ed63..a1923908277d096d5cb1f5a585490ee7e885d0fd 100644 +--- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java ++++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +@@ -46,6 +46,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + } + + private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) { ++ io.papermc.paper.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Paper - region threading + boolean bl = this.count > 0; + boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; + ++this.count; +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index b2845ed8d28627178589da3d2224cd9edd29c31e..5c10849906fcec82a3e92646daab065ac86be58f 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -368,7 +368,7 @@ public class MapItemSavedData extends SavedData { + rotation += rotation < 0.0D ? -8.0D : 8.0D; + b2 = (byte) ((int) (rotation * 16.0D / 360.0D)); + if (this.dimension == Level.NETHER && world != null) { +- int j = (int) (world.getLevelData().getDayTime() / 10L); ++ int j = (int) (world.getLevelData().getDayTime() / 10L); // Paper - region threading - TODO + + b2 = (byte) (j * j * 34187121 + j * 121 >> 15 & 15); + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +index ac807277a6b26d140ea9873d17c7aa4fb5fe37b2..e5305a2773302dab02d8ec7741406b6ace85bc48 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -37,6 +37,21 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + this.dirty = false; + } + // Paper end - add dirty flag ++ // Paper start - region threading ++ public void offsetTicks(final long offset) { ++ if (offset == 0 || this.tickQueue.isEmpty()) { ++ return; ++ } ++ final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); ++ this.tickQueue.clear(); ++ for (final ScheduledTick entry : queue) { ++ final ScheduledTick newEntry = new ScheduledTick<>( ++ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() ++ ); ++ this.tickQueue.add(newEntry); ++ } ++ } ++ // Paper end - region threading + + public LevelChunkTicks() { + } +diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +index 7f1ac2cb29eb84833c0895442d611dfa0504527e..2784ad4f943aeb5b1640e28a40ce2c325627c583 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java +@@ -42,13 +42,70 @@ public class LevelTicks implements LevelTickAccess { + private final List> alreadyRunThisTick = new ArrayList<>(); + private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); + private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (chunkTickScheduler, tick) -> { +- if (tick.equals(chunkTickScheduler.peek())) { +- this.updateContainerScheduling(tick); ++ if (tick.equals(chunkTickScheduler.peek())) { // Paper - diff on change ++ this.updateContainerScheduling(tick); // Paper - diff on change + } + + }; + +- public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter) { ++ // Paper start - region threading ++ public final net.minecraft.server.level.ServerLevel world; ++ public final boolean isBlock; ++ ++ public void merge(final LevelTicks into, final long tickOffset) { ++ // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet ++ // are all transient state, only ever non-empty during tick. But merging regions occurs while there ++ // is no tick happening, so we assume they are empty. ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final LevelChunkTicks tickContainer = entry.getValue(); ++ tickContainer.offsetTicks(tickOffset); ++ into.allContainers.put(entry.getLongKey(), tickContainer); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); ++ } ++ } ++ ++ public void split(final int chunkToRegionShift, ++ final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); ++ } ++ } ++ // Paper end - region threading ++ ++ public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Paper - add world and isBlock + this.tickCheck = tickingFutureReadyPredicate; + this.profiler = profilerGetter; + } +@@ -61,7 +118,17 @@ public class LevelTicks implements LevelTickAccess { + this.nextTickForContainer.put(l, scheduledTick.triggerTick()); + } + +- scheduler.setOnTickAdded(this.chunkScheduleUpdater); ++ // Paper start - region threading ++ final boolean isBlock = this.isBlock; ++ final net.minecraft.server.level.ServerLevel world = this.world; ++ // make sure the lambda contains no reference to this LevelTicks ++ scheduler.setOnTickAdded((chunkTickScheduler, tick) -> { ++ if (tick.equals(chunkTickScheduler.peek())) { ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); ++ (isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks()).updateContainerScheduling((ScheduledTick)tick); ++ } ++ }); ++ // Paper end - region threading + } + + public void removeContainer(ChunkPos pos) { +@@ -76,6 +143,7 @@ public class LevelTicks implements LevelTickAccess { + + @Override + public void schedule(ScheduledTick orderedTick) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, orderedTick.pos(), "Cannot schedule tick for another region!"); // Paper - region threading + long l = ChunkPos.asLong(orderedTick.pos()); + LevelChunkTicks levelChunkTicks = this.allContainers.get(l); + if (levelChunkTicks == null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..c4ce387a09b97b289e5d67919359bfdb2f0af249 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -879,6 +879,9 @@ public final class CraftServer implements Server { + + // NOTE: Should only be called from DedicatedServer.ah() + public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) { ++ // Paper start - region threading ++ io.papermc.paper.threadedregions.RegionisedServer.ensureGlobalTickThread("May not dispatch server commands async"); ++ // Paper end - region threading + if (sender instanceof Conversable) { + Conversable conversable = (Conversable) sender; + +@@ -898,12 +901,44 @@ public final class CraftServer implements Server { + } + } + ++ // Paper start - region threading ++ public void dispatchCmdAsync(CommandSender sender, String commandLine) { ++ if ((sender instanceof Entity entity)) { ++ ((org.bukkit.craftbukkit.entity.CraftEntity)entity).taskScheduler.schedule( ++ (nmsEntity) -> { ++ CraftServer.this.dispatchCommand(nmsEntity.getBukkitEntity(), commandLine); ++ }, ++ null, ++ 1L ++ ); ++ } else if (sender instanceof ConsoleCommandSender console) { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { ++ CraftServer.this.dispatchCommand(sender, commandLine); ++ }); ++ } else { ++ // huh? ++ throw new UnsupportedOperationException("Dispatching command for " + sender); ++ } ++ } ++ // Paper end - region threading ++ + @Override + public boolean dispatchCommand(CommandSender sender, String commandLine) { + Validate.notNull(sender, "Sender cannot be null"); + Validate.notNull(commandLine, "CommandLine cannot be null"); + org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot + ++ // Paper start - region threading ++ if ((sender instanceof Entity entity)) { ++ io.papermc.paper.util.TickThread.ensureTickThread(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle(), "Dispatching command async"); ++ } else if (sender instanceof ConsoleCommandSender console) { ++ io.papermc.paper.threadedregions.RegionisedServer.ensureGlobalTickThread("Dispatching command async"); ++ } else { ++ // huh? ++ throw new UnsupportedOperationException("Dispatching command for " + sender); ++ } ++ // Paper end - region threading ++ + // Paper Start + if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) { + final CommandSender fSender = sender; +@@ -2913,7 +2948,7 @@ public final class CraftServer implements Server { + + @Override + public int getCurrentTick() { +- return net.minecraft.server.MinecraftServer.currentTick; ++ return (int)io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - region threading + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index d33476ffa49d7f6388bb227f8a57cf115a74698f..8eb33972ccd3cb7a860c2497dee752cb794ba503 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -180,7 +180,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getTickableTileEntityCount() { +- return world.getTotalTileEntityTickers(); ++ throw new UnsupportedOperationException(); // Paper - region threading - TODO fix this? + } + + @Override +@@ -788,13 +788,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { +- world.captureTreeGeneration = true; +- world.captureBlockStates = true; ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading ++ worldData.captureTreeGeneration = true; // Paper - region threading ++ worldData.captureBlockStates = true; // Paper - region threading + boolean grownTree = this.generateTree(loc, type); +- world.captureBlockStates = false; +- world.captureTreeGeneration = false; ++ worldData.captureBlockStates = false; // Paper - region threading ++ worldData.captureTreeGeneration = false; // Paper - region threading + if (grownTree) { // Copy block data to delegate +- for (BlockState blockstate : world.capturedBlockStates.values()) { ++ for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Paper - region threading + BlockPos position = ((CraftBlockState) blockstate).getPosition(); + net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position); + int flag = ((CraftBlockState) blockstate).getFlag(); +@@ -802,10 +803,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { + net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position); + this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flag, 512); + } +- world.capturedBlockStates.clear(); ++ worldData.capturedBlockStates.clear(); // Paper - region threading + return true; + } else { +- world.capturedBlockStates.clear(); ++ worldData.capturedBlockStates.clear(); // Paper - region threading + return false; + } + } +@@ -878,7 +879,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public long getGameTime() { +- return world.levelData.getGameTime(); ++ return this.getHandle().getGameTime(); // Paper - region threading + } + + @Override +@@ -1853,7 +1854,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; + + ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(BuiltInRegistries.SOUND_EVENT.wrapAsHolder(CraftSound.getSoundEffect(sound)), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, this.getHandle().getRandom().nextLong()); +- ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().tracker; // Paper - region threading + if (entityTracker != null) { + entityTracker.broadcastAndSend(packet); + } +@@ -2356,7 +2357,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); + + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { +- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue(this.getHandle(), x, z, () -> { // Paper - region threading + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; + if (chunk != null) addTicket(x, z); // Paper + ret.complete(chunk == null ? null : chunk.getBukkitChunk()); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index 350cbf64c17938021002d5fd67176c44b398231e..173f180272de79afe853848f04c8f39e24c807dc 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -568,16 +568,17 @@ public class CraftBlock implements Block { + ServerLevel world = this.getCraftWorld().getHandle(); + UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); + ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - region threading + // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent +- world.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Paper - region threading + InteractionResult result = BoneMealItem.applyBonemeal(context); +- world.captureTreeGeneration = false; ++ worldData.captureTreeGeneration = false; // Paper - region threading + +- if (world.capturedBlockStates.size() > 0) { ++ if (worldData.capturedBlockStates.size() > 0) { // Paper - region threading + TreeType treeType = SaplingBlock.treeType; + SaplingBlock.treeType = null; +- List blocks = new ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ List blocks = new ArrayList<>(worldData.capturedBlockStates.values()); // Paper - region threading ++ worldData.capturedBlockStates.clear(); // Paper - region threading + StructureGrowEvent structureEvent = null; + + if (treeType != null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +index cd4ad8261e56365850068db1d83d6a8454026737..2daae3c5a237ac8e8d74d26e715131a26e870a6c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +@@ -50,7 +50,7 @@ public class ConsoleCommandCompleter implements Completer { + return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of(); + } + }; +- server.getServer().processQueue.add(syncCompletions); ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(syncCompletions); // Paper - region threading + try { + final List legacyCompletions = syncCompletions.get(); + completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed +@@ -98,7 +98,7 @@ public class ConsoleCommandCompleter implements Completer { + return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); + } + }; +- server.getServer().processQueue.add(waitable); // Paper - Remove "this." ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(waitable); // Paper - region threading + try { + List offers = waitable.get(); + if (offers == null) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 78f53ee557276de85f0431ebcb146445b1f4fb92..1723dc6b981a267018e90ecf6f21a74f38367379 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -200,6 +200,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + private EntityDamageEvent lastDamageEvent; + private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY); + protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers ++ public final io.papermc.paper.threadedregions.EntityScheduler taskScheduler = new io.papermc.paper.threadedregions.EntityScheduler(this); // Paper - region threading + + public CraftEntity(final CraftServer server, final Entity entity) { + this.server = server; +@@ -556,6 +557,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + + @Override + public boolean teleport(Location location, TeleportCause cause, boolean ignorePassengers, boolean dismount) { ++ // Paper start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Paper end - region threading + // Paper end + Preconditions.checkArgument(location != null, "location cannot be null"); + location.checkFinite(); +@@ -1206,7 +1212,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); +- ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); ++ ChunkMap.TrackedEntity entityTracker = this.getHandle().tracker; // Paper - region threading + + if (entityTracker == null) { + return; +@@ -1270,30 +1276,38 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + Preconditions.checkArgument(location != null, "location"); + location.checkFinite(); + Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. +- +- net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); ++ // Paper start - region threading + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); +- +- world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), +- this instanceof CraftPlayer ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, (list) -> { +- net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource(); +- for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { +- chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); +- } +- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { +- try { +- ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); +- } catch (Throwable throwable) { +- if (throwable instanceof ThreadDeath) { +- throw (ThreadDeath)throwable; ++ boolean scheduled = this.taskScheduler.schedule( ++ // success ++ (Entity nmsEntity) -> { ++ boolean success = nmsEntity.teleportAsync( ++ ((CraftWorld)locationClone.getWorld()).getHandle(), ++ new net.minecraft.world.phys.Vec3(locationClone.getX(), locationClone.getY(), locationClone.getZ()), ++ null, null, net.minecraft.world.phys.Vec3.ZERO, ++ cause == null ? TeleportCause.UNKNOWN : cause, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ (Entity entityTp) -> { ++ ret.complete(Boolean.TRUE); + } +- net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); +- ret.completeExceptionally(throwable); ++ ); ++ if (!success) { ++ ret.complete(Boolean.FALSE); + } +- }); +- }); ++ }, ++ // retired ++ (Entity nmsEntity) -> { ++ ret.complete(Boolean.FALSE); ++ }, ++ 1L ++ ); ++ ++ if (!scheduled) { ++ ret.complete(Boolean.FALSE); ++ } + + return ret; ++ // Paper end - region threading + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 0351eb67bac6ce257f820af60aa3bba9f45da687..2530d925b969c27358edda71a62d2611260fd5f5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -1702,7 +1702,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + private void unregisterEntity(Entity other) { + // Paper end + ChunkMap tracker = ((ServerLevel) this.getHandle().level).getChunkSource().chunkMap; +- ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); ++ ChunkMap.TrackedEntity entry = other.tracker; // Paper - region threading + if (entry != null) { + entry.removePlayer(this.getHandle()); + } +@@ -1765,7 +1765,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.getHandle().connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(otherPlayer))); + } + +- ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); ++ ChunkMap.TrackedEntity entry = other.tracker; // Paper - region threading + if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { + entry.updatePlayer(this.getHandle()); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 6a52ae70b5f7fd9953b6b2605cae722f606e7fec..8ee60d2b04ce9d93e086a9cce8a7262ea31ee9aa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -228,8 +228,8 @@ import org.bukkit.event.entity.SpawnerSpawnEvent; // Spigot + public class CraftEventFactory { + public static final DamageSource MELTING = CraftDamageSource.copyOf(DamageSource.ON_FIRE); + public static final DamageSource POISON = CraftDamageSource.copyOf(DamageSource.MAGIC); +- public static org.bukkit.block.Block blockDamage; // For use in EntityDamageByBlockEvent +- public static Entity entityDamage; // For use in EntityDamageByEntityEvent ++ public static final ThreadLocal blockDamageRT = new ThreadLocal<>(); // For use in EntityDamageByBlockEvent ++ public static final ThreadLocal entityDamageRT = new ThreadLocal<>(); // For use in EntityDamageByEntityEvent + + // helper methods + private static boolean canBuild(ServerLevel world, Player player, int x, int z) { +@@ -842,7 +842,7 @@ public class CraftEventFactory { + return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2); + } + +- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. ++ public static ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Paper - region threading + public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState block, int flag) { + // Suppress during worldgen + if (!(world instanceof Level)) { +@@ -853,7 +853,7 @@ public class CraftEventFactory { + CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag); + state.setData(block); + +- BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state); ++ BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // Paper - region threading + Bukkit.getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +@@ -968,8 +968,8 @@ public class CraftEventFactory { + private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { + if (source.isExplosion()) { + DamageCause damageCause; +- Entity damager = CraftEventFactory.entityDamage; +- CraftEventFactory.entityDamage = null; ++ Entity damager = CraftEventFactory.entityDamageRT.get(); // Paper - region threading ++ CraftEventFactory.entityDamageRT.set(null); // Paper - region threading + EntityDamageEvent event; + if (damager == null) { + event = new EntityDamageByBlockEvent(null, entity.getBukkitEntity(), DamageCause.BLOCK_EXPLOSION, modifiers, modifierFunctions); +@@ -1022,13 +1022,13 @@ public class CraftEventFactory { + } + return event; + } else if (source == DamageSource.LAVA) { +- EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamage, entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); ++ EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamageRT.get(), entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); // Paper - region threading + event.setCancelled(cancelled); + +- Block damager = CraftEventFactory.blockDamage; +- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call ++ Block damager = CraftEventFactory.blockDamageRT.get(); ++ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Paper - region threading + CraftEventFactory.callEvent(event); +- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause ++ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Paper - region threading + + if (!event.isCancelled()) { + event.getEntity().setLastDamageCause(event); +@@ -1036,9 +1036,9 @@ public class CraftEventFactory { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } + return event; +- } else if (CraftEventFactory.blockDamage != null) { ++ } else if (CraftEventFactory.blockDamageRT.get() != null) { // Paper - region threading + DamageCause cause = null; +- Block damager = CraftEventFactory.blockDamage; ++ Block damager = CraftEventFactory.blockDamageRT.get(); // Paper - region threading + if (source == DamageSource.CACTUS || source == DamageSource.SWEET_BERRY_BUSH || source == DamageSource.STALAGMITE || "fallingStalactite".equals(source.msgId) || "anvil".equals(source.msgId)) { + cause = DamageCause.CONTACT; + } else if (source == DamageSource.HOT_FLOOR) { +@@ -1053,9 +1053,9 @@ public class CraftEventFactory { + EntityDamageEvent event = new EntityDamageByBlockEvent(damager, entity.getBukkitEntity(), cause, modifiers, modifierFunctions); + event.setCancelled(cancelled); + +- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call ++ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Paper - region threading + CraftEventFactory.callEvent(event); +- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause ++ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Paper - region threading + + if (!event.isCancelled()) { + event.getEntity().setLastDamageCause(event); +@@ -1063,10 +1063,10 @@ public class CraftEventFactory { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } + return event; +- } else if (CraftEventFactory.entityDamage != null) { ++ } else if (CraftEventFactory.entityDamageRT.get() != null) { // Paper - region threading + DamageCause cause = null; +- CraftEntity damager = CraftEventFactory.entityDamage.getBukkitEntity(); +- CraftEventFactory.entityDamage = null; ++ CraftEntity damager = CraftEventFactory.entityDamageRT.get().getBukkitEntity(); ++ CraftEventFactory.entityDamageRT.set(null); // Paper - region threading + if ("fallingStalactite".equals(source.msgId) || "fallingBlock".equals(source.msgId) || "anvil".equals(source.msgId)) { + cause = DamageCause.FALLING_BLOCK; + } else if (damager instanceof LightningStrike) { +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index cdefb2025eedea7e204d70d568adaf1c1ec4c03c..7ae3f2da090e6596b9f52762cd7bcc13bf7d0a99 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -533,6 +533,7 @@ public class CraftScheduler implements BukkitScheduler { + } + + protected CraftTask handle(final CraftTask task, final long delay) { // Paper ++ if (true) throw new UnsupportedOperationException(); // Paper - region threading + // Paper start + if (!this.isAsyncScheduler && !task.isSync()) { + this.asyncScheduler.handle(task, delay); +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index e881584d38dc354204479863f004e974a0ac6c07..589b4093a0243b3fab2febcafc36b3d7d5c84eb2 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -65,26 +65,27 @@ public class ActivationRange + + private static int checkInactiveWakeup(Entity entity) { + Level world = entity.level; ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - threaded regions + SpigotWorldConfig config = world.spigotConfig; +- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ long inactiveFor = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() - entity.activatedTick; // Paper - threaded regions + if (entity.activationType == ActivationType.VILLAGER) { +- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { +- world.wakeupInactiveRemainingVillagers--; ++ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Paper - threaded regions ++ worldData.wakeupInactiveRemainingVillagers--; // Paper - threaded regions + return config.wakeUpInactiveVillagersFor; + } + } else if (entity.activationType == ActivationType.ANIMAL) { +- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { +- world.wakeupInactiveRemainingAnimals--; ++ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Paper - threaded regions ++ worldData.wakeupInactiveRemainingAnimals--; // Paper - threaded regions + return config.wakeUpInactiveAnimalsFor; + } + } else if (entity.activationType == ActivationType.FLYING_MONSTER) { +- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { +- world.wakeupInactiveRemainingFlying--; ++ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Paper - threaded regions ++ worldData.wakeupInactiveRemainingFlying--; // Paper - threaded regions + return config.wakeUpInactiveFlyingFor; + } + } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { +- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { +- world.wakeupInactiveRemainingMonsters--; ++ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Paper - threaded regions ++ worldData.wakeupInactiveRemainingMonsters--; // Paper - threaded regions + return config.wakeUpInactiveMonstersFor; + } + } +@@ -174,10 +175,11 @@ public class ActivationRange + final int waterActivationRange = world.spigotConfig.waterActivationRange; + final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; + final int villagerActivationRange = world.spigotConfig.villagerActivationRange; +- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); +- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); +- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); +- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); ++ io.papermc.paper.threadedregions.RegionisedWorldData worldData = world.getCurrentWorldData(); // Paper - threaded regions ++ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Paper - threaded regions ++ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Paper - threaded regions ++ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Paper - threaded regions ++ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Paper - threaded regions + final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); + // Paper end + +@@ -191,9 +193,9 @@ public class ActivationRange + // Paper end + maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); + +- for ( Player player : world.players() ) ++ for ( Player player : world.getLocalPlayers() ) // Paper - region threading + { +- player.activatedTick = MinecraftServer.currentTick; ++ player.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - region threading + if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() ) + { + continue; +@@ -215,7 +217,7 @@ public class ActivationRange + java.util.List entities = world.getEntities((Entity)null, maxBB, (e) -> !(e instanceof net.minecraft.world.entity.Marker)); // Don't tick markers + for (int i = 0; i < entities.size(); i++) { + Entity entity = entities.get(i); +- ActivationRange.activateEntity(entity); ++ if (io.papermc.paper.util.TickThread.isTickThreadFor(entity)) ActivationRange.activateEntity(entity); // Paper - region ticking + } + // Paper end + } +@@ -229,16 +231,16 @@ public class ActivationRange + */ + private static void activateEntity(Entity entity) + { +- if ( MinecraftServer.currentTick > entity.activatedTick ) ++ if ( io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() > entity.activatedTick ) // Paper - threaded regions + { + if ( entity.defaultActivationState ) + { +- entity.activatedTick = MinecraftServer.currentTick; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - threaded regions + return; + } + if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) + { +- entity.activatedTick = MinecraftServer.currentTick; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - threaded regions + } + } + } +@@ -261,10 +263,10 @@ public class ActivationRange + if (entity.remainingFireTicks > 0) { + return 2; + } +- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { ++ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick()) { // Paper - threaded regions + return 1; + } +- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ long inactiveFor = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() - entity.activatedTick; // Paper - threaded regions + // Paper end + // quick checks. + if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper +@@ -387,19 +389,19 @@ public class ActivationRange + } + // Paper end + +- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; ++ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Paper - threaded regions + entity.isTemporarilyActive = false; // Paper + + // Should this entity tick? + if ( !isActive ) + { +- if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) ++ if ( ( io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() - entity.activatedTick - 1 ) % 20 == 0 ) // Paper - threaded regions + { + // Check immunities every 20 ticks. + // Paper start + int immunity = checkEntityImmunities(entity); + if (immunity >= 0) { +- entity.activatedTick = MinecraftServer.currentTick + immunity; ++ entity.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + immunity; // Paper - threaded regions + } else { + entity.isTemporarilyActive = true; + } +diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java +index 3112a8695639c402e9d18710acbc11cff5611e9c..e113e9a5f7acf6e7310d367e32edb35a7c272d06 100644 +--- a/src/main/java/org/spigotmc/SpigotCommand.java ++++ b/src/main/java/org/spigotmc/SpigotCommand.java +@@ -29,6 +29,7 @@ public class SpigotCommand extends Command { + Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); + Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); + ++ io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Paper - region threading + MinecraftServer console = MinecraftServer.getServer(); + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); + for (ServerLevel world : console.getAllLevels()) { +@@ -37,6 +38,7 @@ public class SpigotCommand extends Command { + console.server.reloadCount++; + + Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete."); ++ }); // Paper - region threading + } + + return true; +diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java +index 612c3169c3463d702b85975e1db79ae6e47d60d0..65441b5226255f85b9f790b9e7b22d209fba74b5 100644 +--- a/src/main/java/org/spigotmc/SpigotConfig.java ++++ b/src/main/java/org/spigotmc/SpigotConfig.java +@@ -228,7 +228,7 @@ public class SpigotConfig + SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash ); + SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript ); + SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) ); +- SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); ++ //SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); // Paper - region threading + // WatchdogThread.doStart( timeoutTime, restartOnCrash ); // Paper - moved to after paper config initialization + } + +@@ -283,7 +283,7 @@ public class SpigotConfig + + private static void tpsCommand() + { +- SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); ++ //SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); // Paper - region threading + } + + public static int playerSample; +diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java +index 5503ad6a93d331771a0e92c0da6adedf2ac81aff..9be6a8acf4924cc9480c57f1eea418536404c8fd 100644 +--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java ++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java +@@ -425,7 +425,7 @@ public class SpigotWorldConfig + this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 ); + } + +- public int currentPrimedTnt = 0; ++ //public int currentPrimedTnt = 0; // Paper - region threading - moved to regionised world data + public int maxTntTicksPerTick; + private void maxTntPerTick() { + if ( SpigotConfig.version < 7 ) diff --git a/settings.gradle.kts b/settings.gradle.kts index b50ecd4..769f27b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,6 @@ pluginManagement { } } -rootProject.name = "forktest" +rootProject.name = "Folia" -include("forktest-api", "forktest-server") +include("Folia-API", "Folia-Server")