aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch')
-rw-r--r--patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch1216
1 files changed, 1216 insertions, 0 deletions
diff --git a/patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch b/patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
new file mode 100644
index 0000000000..1b34693479
--- /dev/null
+++ b/patches/server/0466-Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
@@ -0,0 +1,1216 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Sat, 11 Apr 2020 03:56:07 -0400
+Subject: [PATCH] Implement Chunk Priority / Urgency System for Chunks
+
+Mark chunks that are blocking main thread for world generation as urgent
+
+Implements a general priority system so that chunks that are sorted in
+the generator queues can prioritize certain chunks over another.
+
+Urgent chunks will jump to the front of the line, ensuring that a
+sync chunk load on an ungenerated chunk does not lag the server for
+a long period of time if the servers generator queues are filled with
+lots of chunks already.
+
+This massively reduces the lag spikes from sync chunk gens.
+
+Then we further prioritize loading order so nearby chunks have higher
+priority than distant chunks, reducing the pressure a high no tick
+view distance holds on you.
+
+Chunks in front of the player have higher priority, to help with
+fast traveling players keep up with their movement.
+
+diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
+index 80c785eb503dc36d381f114a4eccffc1a81071f7..311a01d3590dabc7a4e41bb3493cd7ff228a515c 100644
+--- a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
+@@ -106,7 +106,7 @@ public final class ChunkTaskManager {
+ }
+
+ static void dumpChunkInfo(Set<ChunkHolder> seenChunks, ChunkHolder chunkHolder, int x, int z) {
+- dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1);
++ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 4); // Paper - 1->4
+ }
+
+ static void dumpChunkInfo(Set<ChunkHolder> seenChunks, ChunkHolder chunkHolder, int x, int z, int indent, int maxDepth) {
+@@ -127,6 +127,31 @@ public final class ChunkTaskManager {
+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getStatus().toString()));
+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Ticket Status - " + ChunkHolder.getStatus(chunkHolder.getTicketLevel()));
+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString()));
++ // Paper start
++ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder Priority - " + chunkHolder.queueLevel);
++
++ if (!chunkHolder.neighbors.isEmpty()) {
++ if (indent >= maxDepth) {
++ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Neighbors: (Can't show, too deeply nested)");
++ return;
++ }
++ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Neighbors: ");
++ for (ChunkHolder neighbor : chunkHolder.neighbors.keySet()) {
++ ChunkStatus status = neighbor.getChunkHolderStatus();
++ if (status != null && status.isOrAfter(ChunkHolder.getStatus(neighbor.getTicketLevel()))) {
++ continue;
++ }
++ int nx = neighbor.pos.x;
++ int nz = neighbor.pos.z;
++ if (seenChunks.contains(neighbor)) {
++ PaperFileIOThread.LOGGER.error(indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + " (CIRCULAR)");
++ continue;
++ }
++ PaperFileIOThread.LOGGER.error(indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + ":");
++ dumpChunkInfo(seenChunks, neighbor, nx, nz, indent + 1, maxDepth);
++ }
++ }
++ // Paper end
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
+index 89e0181af99cba2368f875fc192342efc972f2ef..b3516862d796c2d9fcc1c67a6073445403d73088 100644
+--- a/src/main/java/net/minecraft/server/MCUtil.java
++++ b/src/main/java/net/minecraft/server/MCUtil.java
+@@ -681,6 +681,7 @@ public final class MCUtil {
+ chunkData.addProperty("x", playerChunk.pos.x);
+ chunkData.addProperty("z", playerChunk.pos.z);
+ chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
++ chunkData.addProperty("priority", playerChunk.queueLevel); // Paper - priority
+ chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString());
+ chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey));
+ chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 05d2790b80a6d2e1dc6b8d2375f783be4eff2343..6ba7e2713452c4c6f48a1a825ef27b500140aa16 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -60,7 +60,7 @@ public class ChunkHolder {
+ private final DebugBuffer<ChunkHolder.ChunkSaveDebug> chunkToSaveHistory;
+ public int oldTicketLevel;
+ private int ticketLevel;
+- private int queueLevel;
++ public volatile int queueLevel; // Paper - private->public, make volatile since this is concurrently accessed
+ public final ChunkPos pos;
+ private boolean hasChangedSections;
+ private final ShortSet[] changedBlocksPerSection;
+@@ -73,6 +73,7 @@ public class ChunkHolder {
+ private boolean resendLight;
+ private CompletableFuture<Void> pendingFullStateConfirmation;
+
++ public ServerLevel getWorld() { return chunkMap.level; } // Paper
+ boolean isUpdateQueued = false; // Paper
+ private final ChunkMap chunkMap; // Paper
+
+@@ -437,12 +438,18 @@ public class ChunkHolder {
+ });
+ }
+
++ // Paper start
++ private boolean loadCallbackScheduled = false;
++ private boolean unloadCallbackScheduled = false;
++ // Paper end
++
+ private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) {
+ this.pendingFullStateConfirmation.cancel(false);
+ playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
+ }
+
+ protected void updateFutures(ChunkMap chunkStorage, Executor executor) {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async ticket level update"); // Paper
+ ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel);
+ ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel);
+ boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE;
+@@ -453,9 +460,22 @@ public class ChunkHolder {
+ // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
+ if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
+ this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper
+ LevelChunk chunk = (LevelChunk)either.left().orElse(null);
+- if (chunk != null) {
++ if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Paper - only invoke unload if load was called
++ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
++ if (ChunkHolder.this.unloadCallbackScheduled) {
++ return;
++ }
++ ChunkHolder.this.unloadCallbackScheduled = true;
++ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
+ chunkStorage.callbackExecutor.execute(() -> {
++ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
++ ChunkHolder.this.unloadCallbackScheduled = false;
++ if (ChunkHolder.this.ticketLevel <= 33) {
++ return;
++ }
++ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
+ // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
+ // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
+ // These actions may however happen deferred, so we manually set the needsSaving flag already here.
+@@ -512,12 +532,14 @@ public class ChunkHolder {
+ this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER);
+ // Paper start - cache ticking ready status
+ this.fullChunkFuture.thenAccept(either -> {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
+ final Optional<LevelChunk> left = either.left();
+ if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ LevelChunk fullChunk = either.left().get();
+ ChunkHolder.this.isFullChunkReady = true;
+ fullChunk.playerChunk = ChunkHolder.this;
++ this.chunkMap.distanceManager.clearPriorityTickets(pos);
+ }
+ });
+ this.updateChunkToSave(this.fullChunkFuture, "full");
+@@ -538,6 +560,7 @@ public class ChunkHolder {
+ this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING);
+ // Paper start - cache ticking ready status
+ this.tickingChunkFuture.thenAccept(either -> {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
+ either.ifLeft(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
+@@ -573,6 +596,7 @@ public class ChunkHolder {
+ this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING);
+ // Paper start - cache ticking ready status
+ this.entityTickingChunkFuture.thenAccept(either -> {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
+ either.ifLeft(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
+ // Paper start - entity ticking chunk set
+@@ -599,16 +623,45 @@ public class ChunkHolder {
+ this.demoteFullChunk(chunkStorage, playerchunk_state1);
+ }
+
+- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
++ //this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
++ // Paper start - raise IO/load priority if priority changes, use our preferred priority
++ priorityBoost = chunkMap.distanceManager.getChunkPriority(pos);
++ int currRequestedPriority = this.requestedPriority;
++ int priority = getDemandedPriority();
++ int newRequestedPriority = this.requestedPriority = priority;
++ if (this.queueLevel > priority) {
++ int ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY;
++ if (priority <= 10) {
++ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
++ } else if (priority <= 20) {
++ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY;
++ }
++ chunkMap.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, ioPriority);
++ chunkMap.level.getChunkSource().getLightEngine().queue.changePriority(pos.toLong(), this.queueLevel, priority); // Paper // Restore this in chunk priority later?
++ }
++ if (currRequestedPriority != newRequestedPriority) {
++ this.onLevelChange.onLevelChange(this.pos, () -> this.queueLevel, priority, p -> this.queueLevel = p); // use preferred priority
++ int neighborsPriority = getNeighborsPriority();
++ this.neighbors.forEach((neighbor, neighborDesired) -> neighbor.setNeighborPriority(this, neighborsPriority));
++ }
++ // Paper end
+ this.oldTicketLevel = this.ticketLevel;
+ // CraftBukkit start
+ // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
+ if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
+ this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
++ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper
+ LevelChunk chunk = (LevelChunk)either.left().orElse(null);
+- if (chunk != null) {
++ if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Paper - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33
++ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded...
++ if (ChunkHolder.this.loadCallbackScheduled) {
++ return;
++ }
++ ChunkHolder.this.loadCallbackScheduled = true;
++ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded...
+ chunkStorage.callbackExecutor.execute(() -> {
+- chunk.loadCallback();
++ ChunkHolder.this.loadCallbackScheduled = false; // Paper - only schedule once, now the future is no longer completed as RIGHT if unloaded...
++ if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Paper "
+ });
+ }
+ }).exceptionally((throwable) -> {
+@@ -733,7 +786,134 @@ public class ChunkHolder {
+ };
+ }
+
+- // Paper start
++ // Paper start - Chunk gen/load priority system
++ volatile int neighborPriority = -1;
++ volatile int priorityBoost = 0;
++ public final java.util.concurrent.ConcurrentHashMap<ChunkHolder, ChunkStatus> neighbors = new java.util.concurrent.ConcurrentHashMap<>();
++ public final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<Integer> neighborPriorities = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
++ int requestedPriority = ChunkMap.MAX_CHUNK_DISTANCE + 1; // this priority is possible pending, but is used to ensure needless updates are not queued
++
++ private int getDemandedPriority() {
++ int priority = neighborPriority; // if we have a neighbor priority, use it
++ int myPriority = getMyPriority();
++
++ if (priority == -1 || (ticketLevel <= 33 && priority > myPriority)) {
++ priority = myPriority;
++ }
++
++ return Math.max(1, Math.min(Math.max(ticketLevel, ChunkMap.MAX_CHUNK_DISTANCE), priority));
++ }
++
++ private int getMyPriority() {
++ if (priorityBoost == DistanceManager.URGENT_PRIORITY) {
++ return 2; // Urgent - ticket level isn't always 31 so 33-30 = 3, but allow 1 more tasks to go below this for dependents
++ }
++ return ticketLevel - priorityBoost;
++ }
++
++ private int getNeighborsPriority() {
++ return (neighborPriorities.isEmpty() ? getMyPriority() : getDemandedPriority()) + 1;
++ }
++
++ public void onNeighborRequest(ChunkHolder neighbor, ChunkStatus status) {
++ neighbor.setNeighborPriority(this, getNeighborsPriority());
++ this.neighbors.compute(neighbor, (playerChunk, currentWantedStatus) -> {
++ if (currentWantedStatus == null || !currentWantedStatus.isOrAfter(status)) {
++ //System.out.println(this + " request " + neighbor + " at " + status + " currently " + currentWantedStatus);
++ return status;
++ } else {
++ //System.out.println(this + " requested " + neighbor + " at " + status + " but thats lower than other wanted status " + currentWantedStatus);
++ return currentWantedStatus;
++ }
++ });
++
++ }
++
++ public void onNeighborDone(ChunkHolder neighbor, ChunkStatus chunkstatus, ChunkAccess chunk) {
++ this.neighbors.compute(neighbor, (playerChunk, wantedStatus) -> {
++ if (wantedStatus != null && chunkstatus.isOrAfter(wantedStatus)) {
++ //System.out.println(this + " neighbor done at " + neighbor + " for status " + chunkstatus + " wanted " + wantedStatus);
++ neighbor.removeNeighborPriority(this);
++ return null;
++ } else {
++ //System.out.println(this + " neighbor finished our previous request at " + neighbor + " for status " + chunkstatus + " but we now want instead " + wantedStatus);
++ return wantedStatus;
++ }
++ });
++ }
++
++ private void removeNeighborPriority(ChunkHolder requester) {
++ synchronized (neighborPriorities) {
++ neighborPriorities.remove(requester.pos.toLong());
++ recalcNeighborPriority();
++ }
++ checkPriority();
++ }
++
++
++ private void setNeighborPriority(ChunkHolder requester, int priority) {
++ synchronized (neighborPriorities) {
++ if (!Integer.valueOf(priority).equals(neighborPriorities.put(requester.pos.toLong(), Integer.valueOf(priority)))) {
++ recalcNeighborPriority();
++ }
++ }
++ checkPriority();
++ }
++
++ private void recalcNeighborPriority() {
++ neighborPriority = -1;
++ if (!neighborPriorities.isEmpty()) {
++ synchronized (neighborPriorities) {
++ for (Integer neighbor : neighborPriorities.values()) {
++ if (neighbor < neighborPriority || neighborPriority == -1) {
++ neighborPriority = neighbor;
++ }
++ }
++ }
++ }
++ }
++ private void checkPriority() {
++ if (this.requestedPriority != getDemandedPriority()) this.chunkMap.queueHolderUpdate(this);
++ }
++
++ public final double getDistance(ServerPlayer player) {
++ return getDistance(player.getX(), player.getZ());
++ }
++ public final double getDistance(double blockX, double blockZ) {
++ int cx = net.minecraft.server.MCUtil.fastFloor(blockX) >> 4;
++ int cz = net.minecraft.server.MCUtil.fastFloor(blockZ) >> 4;
++ final double x = pos.x - cx;
++ final double z = pos.z - cz;
++ return (x * x) + (z * z);
++ }
++
++ public final double getDistanceFrom(BlockPos pos) {
++ return getDistance(pos.getX(), pos.getZ());
++ }
++
++ public static ChunkStatus getNextStatus(ChunkStatus status) {
++ if (status == ChunkStatus.FULL) {
++ return status;
++ }
++ return CHUNK_STATUSES.get(status.getIndex() + 1);
++ }
++ public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getStatusFutureUncheckedMain(ChunkStatus chunkstatus) {
++ return ensureMain(getFutureIfPresentUnchecked(chunkstatus));
++ }
++ public <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
++ return future.thenApplyAsync(r -> r, chunkMap.mainInvokingExecutor);
++ }
++
++ @Override
++ public String toString() {
++ return "PlayerChunk{" +
++ "location=" + pos +
++ ", ticketLevel=" + ticketLevel + "/" + getStatus(this.ticketLevel) +
++ ", chunkHolderStatus=" + getChunkHolderStatus() +
++ ", neighborPriority=" + getNeighborsPriority() +
++ ", priority=(" + ticketLevel + " - " + priorityBoost +" vs N " + neighborPriority + ") = " + getDemandedPriority() + " A " + queueLevel +
++ '}';
++ }
+ public final boolean isEntityTickingReady() {
+ return this.isEntityTickingReady;
+ }
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 85a05c9e59ae1909e6d4ce7a2e45b16366a1b7dc..17af44ebe972158a4fa7b0cb5ea67406ec7bc5ac 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -131,6 +131,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ public final ServerLevel level;
+ private final ThreadedLevelLightEngine lightEngine;
+ private final BlockableEventLoop<Runnable> mainThreadExecutor;
++ final java.util.concurrent.Executor mainInvokingExecutor; // Paper
+ public ChunkGenerator generator;
+ public final Supplier<DimensionDataStorage> overworldDataStorage;
+ private final PoiManager poiManager;
+@@ -320,6 +321,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.level = world;
+ this.generator = chunkGenerator;
+ this.mainThreadExecutor = mainThreadExecutor;
++ // Paper start
++ this.mainInvokingExecutor = (run) -> {
++ if (MCUtil.isMainThread()) {
++ run.run();
++ } else {
++ mainThreadExecutor.execute(run);
++ }
++ };
++ // Paper end
+ ProcessorMailbox<Runnable> threadedmailbox = ProcessorMailbox.create(executor, "worldgen");
+
+ Objects.requireNonNull(mainThreadExecutor);
+@@ -431,6 +441,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ });
+ }
+
++ // Paper start - Chunk Prioritization
++ public void queueHolderUpdate(ChunkHolder playerchunk) {
++ Runnable runnable = () -> {
++ if (isUnloading(playerchunk)) {
++ return; // unloaded
++ }
++ distanceManager.pendingChunkUpdates.add(playerchunk);
++ if (!distanceManager.pollingPendingChunkUpdates) {
++ level.getChunkSource().runDistanceManagerUpdates();
++ }
++ };
++ if (MCUtil.isMainThread()) {
++ // We can't use executor here because it will not execute tasks if its currently in the middle of executing tasks...
++ runnable.run();
++ } else {
++ mainThreadExecutor.execute(runnable);
++ }
++ }
++
++ private boolean isUnloading(ChunkHolder playerchunk) {
++ return playerchunk == null || toDrop.contains(playerchunk.pos.toLong());
++ }
++
++ private void updateChunkPriorityMap(it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap map, long chunk, int level) {
++ int prev = map.getOrDefault(chunk, -1);
++ if (level > prev) {
++ map.put(chunk, level);
++ }
++ }
++ // Paper end
++
+ // Paper start
+ public void updatePlayerMobTypeMap(Entity entity) {
+ if (!this.level.paperConfig.perPlayerMobSpawns) {
+@@ -541,6 +582,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ List<ChunkHolder> list1 = new ArrayList();
+ int j = centerChunk.x;
+ int k = centerChunk.z;
++ ChunkHolder requestingNeighbor = getUpdatingChunkIfPresent(centerChunk.toLong()); // Paper
+
+ for (int l = -margin; l <= margin; ++l) {
+ for (int i1 = -margin; i1 <= margin; ++i1) {
+@@ -559,6 +601,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(j1);
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = playerchunk.getOrScheduleFuture(chunkstatus, this);
++ // Paper start
++ if (requestingNeighbor != null && requestingNeighbor != playerchunk && !completablefuture.isDone()) {
++ requestingNeighbor.onNeighborRequest(playerchunk, chunkstatus);
++ completablefuture.thenAccept(either -> {
++ requestingNeighbor.onNeighborDone(playerchunk, chunkstatus, either.left().orElse(null));
++ });
++ }
++ // Paper end
+
+ list1.add(playerchunk);
+ list.add(completablefuture);
+@@ -930,11 +980,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ if (requiredStatus == ChunkStatus.EMPTY) {
+ return this.scheduleChunkLoad(chunkcoordintpair);
+ } else {
++ // Paper start - revert 1.17 chunk system changes
++ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this);
++ return future.thenComposeAsync((either) -> {
++ Optional<ChunkAccess> optional = either.left();
++ if (!optional.isPresent()) {
++ return CompletableFuture.completedFuture(either);
++ }
++ // Paper end - revert 1.17 chunk system changes
+ if (requiredStatus == ChunkStatus.LIGHT) {
+ this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair);
+ }
+
+- Optional<ChunkAccess> optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left();
++ // Paper - revert 1.17 chunk system changes
+
+ if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) {
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> {
+@@ -946,6 +1004,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ } else {
+ return this.scheduleChunkGeneration(holder, requiredStatus);
+ }
++ }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Paper - revert 1.17 chunk system changes
+ }
+ }
+
+@@ -1002,14 +1061,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ };
+
+ CompletableFuture<CompoundTag> chunkSaveFuture = this.level.asyncChunkTaskManager.getChunkSaveFuture(pos.x, pos.z);
++ // Paper start
++ ChunkHolder playerChunk = getUpdatingChunkIfPresent(pos.toLong());
++ int chunkPriority = playerChunk != null ? playerChunk.requestedPriority : 33;
++ int priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY;
++
++ if (chunkPriority <= 10) {
++ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
++ } else if (chunkPriority <= 20) {
++ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY;
++ }
++ boolean isHighestPriority = priority == com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY;
++ // Paper end
+ if (chunkSaveFuture != null) {
+- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
+- com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture);
+- this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY);
++ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority, chunkSaveFuture); // Paper
+ } else {
+- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
+- com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false);
++ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority); // Paper
+ }
++ this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, priority); // Paper
+ return ret;
+ // Paper end
+ }
+@@ -1061,7 +1130,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.releaseLightTicket(chunkcoordintpair);
+ return CompletableFuture.completedFuture(Either.right(playerchunk_failure));
+ });
+- }, executor);
++ }, executor).thenComposeAsync((either) -> { // Paper start - force competion on the main thread
++ return CompletableFuture.completedFuture(either);
++ }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute
++ // Paper end - force competion on the main thread
+ }
+
+ protected void releaseLightTicket(ChunkPos pos) {
+@@ -1145,7 +1217,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ long i = chunkHolder.getPos().toLong();
+
+ Objects.requireNonNull(chunkHolder);
+- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, chunkHolder::getTicketLevel));
++ mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, () -> 1)); // Paper - final loads are always urgent!
+ });
+ }
+
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index 211566dada0f820af331695b4c62035b89f25a53..d2865ce0523b74aaa935db72c6f3478894e13408 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -128,6 +128,7 @@ public abstract class DistanceManager {
+ }
+
+ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getTicketLevelAt"); // Paper
+ return !tickets.isEmpty() ? ((Ticket) tickets.first()).getTicketLevel() : ChunkMap.MAX_CHUNK_DISTANCE + 1;
+ }
+
+@@ -142,6 +143,7 @@ public abstract class DistanceManager {
+ public boolean runAllUpdates(ChunkMap chunkStorage) {
+ //this.f.a(); // Paper - no longer used
+ this.tickingTicketsTracker.runAllUpdates();
++ org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
+ this.playerTicketManager.runAllUpdates();
+ int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
+ boolean flag = i != 0;
+@@ -152,11 +154,13 @@ public abstract class DistanceManager {
+
+ // Paper start
+ if (!this.pendingChunkUpdates.isEmpty()) {
++ this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority
+ while(!this.pendingChunkUpdates.isEmpty()) {
+ ChunkHolder remove = this.pendingChunkUpdates.remove();
+ remove.isUpdateQueued = false;
+ remove.updateFutures(chunkStorage, this.mainThreadExecutor);
+ }
++ } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority
+ // Paper end
+ return true;
+ } else {
+@@ -192,8 +196,10 @@ public abstract class DistanceManager {
+ return flag;
+ }
+ }
++ boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority
+
+ boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper
+ SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
+ int j = DistanceManager.getTicketLevelAt(arraysetsorted);
+ Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
+@@ -207,7 +213,9 @@ public abstract class DistanceManager {
+ }
+
+ boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper
+ SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
++ int oldLevel = getTicketLevelAt(arraysetsorted); // Paper
+
+ boolean removed = false; // CraftBukkit
+ if (arraysetsorted.remove(ticket)) {
+@@ -239,7 +247,12 @@ public abstract class DistanceManager {
+ this.tickets.remove(i);
+ }
+
+- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
++ // Paper start - Chunk priority
++ int newLevel = getTicketLevelAt(arraysetsorted);
++ if (newLevel > oldLevel) {
++ this.ticketTracker.update(i, newLevel, false);
++ }
++ // Paper end
+ return removed; // CraftBukkit
+ }
+
+@@ -289,6 +302,112 @@ public abstract class DistanceManager {
+ });
+ }
+
++ // Paper start - Chunk priority
++ public static final int PRIORITY_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE;
++ public static final int URGENT_PRIORITY = 29;
++ public boolean delayDistanceManagerTick = false;
++ public boolean markUrgent(ChunkPos coords) {
++ return addPriorityTicket(coords, TicketType.URGENT, URGENT_PRIORITY);
++ }
++ public boolean markHighPriority(ChunkPos coords, int priority) {
++ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority));
++ return addPriorityTicket(coords, TicketType.PRIORITY, priority);
++ }
++
++ public void markAreaHighPriority(ChunkPos center, int priority, int radius) {
++ delayDistanceManagerTick = true;
++ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority));
++ int finalPriority = priority;
++ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> {
++ addPriorityTicket(coords, TicketType.PRIORITY, finalPriority);
++ });
++ delayDistanceManagerTick = false;
++ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
++ }
++
++ public void clearAreaPriorityTickets(ChunkPos center, int radius) {
++ delayDistanceManagerTick = true;
++ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> {
++ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
++ });
++ delayDistanceManagerTick = false;
++ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
++ }
++
++ private boolean addPriorityTicket(ChunkPos coords, TicketType<ChunkPos> ticketType, int priority) {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket");
++ long pair = coords.toLong();
++ ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair);
++ if ((chunk != null && chunk.isFullChunkReady())) {
++ return false;
++ }
++
++ boolean success;
++ if (!(success = updatePriorityTicket(coords, ticketType, priority))) {
++ Ticket<ChunkPos> ticket = new Ticket<ChunkPos>(ticketType, PRIORITY_TICKET_LEVEL, coords);
++ ticket.priority = priority;
++ success = this.addTicket(pair, ticket);
++ } else {
++ if (chunk == null) {
++ chunk = chunkMap.getUpdatingChunkIfPresent(pair);
++ }
++ chunkMap.queueHolderUpdate(chunk);
++ }
++
++ //chunkMap.world.getWorld().spawnParticle(priority <= 15 ? org.bukkit.Particle.EXPLOSION_HUGE : org.bukkit.Particle.EXPLOSION_NORMAL, chunkMap.world.getWorld().getPlayers(), null, coords.x << 4, 70, coords.z << 4, 2, 0, 0, 0, 1, null, true);
++
++ chunkMap.level.getChunkSource().runDistanceManagerUpdates();
++
++ return success;
++ }
++
++ private boolean updatePriorityTicket(ChunkPos coords, TicketType<ChunkPos> type, int priority) {
++ SortedArraySet<Ticket<?>> tickets = this.tickets.get(coords.toLong());
++ if (tickets == null) {
++ return false;
++ }
++ for (Ticket<?> ticket : tickets) {
++ if (ticket.getType() == type) {
++ // We only support increasing, not decreasing, too complicated
++ ticket.setCreatedTick(this.ticketTickCounter);
++ ticket.priority = Math.max(ticket.priority, priority);
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ public int getChunkPriority(ChunkPos coords) {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getChunkPriority");
++ SortedArraySet<Ticket<?>> tickets = this.tickets.get(coords.toLong());
++ if (tickets == null) {
++ return 0;
++ }
++ for (Ticket<?> ticket : tickets) {
++ if (ticket.getType() == TicketType.URGENT) {
++ return URGENT_PRIORITY;
++ }
++ }
++ for (Ticket<?> ticket : tickets) {
++ if (ticket.getType() == TicketType.PRIORITY && ticket.priority > 0) {
++ return ticket.priority;
++ }
++ }
++ return 0;
++ }
++
++ public void clearPriorityTickets(ChunkPos coords) {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearPriority");
++ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords));
++ }
++
++ public void clearUrgent(ChunkPos coords) {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearUrgent");
++ this.removeTicket(coords.toLong(), new Ticket<ChunkPos>(TicketType.URGENT, PRIORITY_TICKET_LEVEL, coords));
++ }
++ // Paper end
++
+ protected void updateChunkForced(ChunkPos pos, boolean forced) {
+ Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, 31, pos);
+ long i = pos.toLong();
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 9fdd2a39e3590b3098fa31b9b0e0081160819630..1b81ae617a43aa4723a879c150ce8e611c5369d9 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -597,6 +597,26 @@ public class ServerChunkCache extends ChunkSource {
+ return CompletableFuture.completedFuture(either);
+ }, this.mainThreadProcessor);
+ }
++
++ public boolean markUrgent(ChunkPos coords) {
++ return this.distanceManager.markUrgent(coords);
++ }
++
++ public boolean markHighPriority(ChunkPos coords, int priority) {
++ return this.distanceManager.markHighPriority(coords, priority);
++ }
++
++ public void markAreaHighPriority(ChunkPos center, int priority, int radius) {
++ this.distanceManager.markAreaHighPriority(center, priority, radius);
++ }
++
++ public void clearAreaPriorityTickets(ChunkPos center, int radius) {
++ this.distanceManager.clearAreaPriorityTickets(center, radius);
++ }
++
++ public void clearPriorityTickets(ChunkPos coords) {
++ this.distanceManager.clearPriorityTickets(coords);
++ }
+ // Paper end - async chunk io
+
+ @Nullable
+@@ -637,6 +657,8 @@ public class ServerChunkCache extends ChunkSource {
+ Objects.requireNonNull(completablefuture);
+ if (!completablefuture.isDone()) { // Paper
+ // Paper start - async chunk io/loading
++ ChunkPos pair = new ChunkPos(x1, z1); // Paper - Chunk priority
++ this.distanceManager.markUrgent(pair); // Paper - Chunk priority
+ this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1);
+ // Paper end
+@@ -645,6 +667,8 @@ public class ServerChunkCache extends ChunkSource {
+ chunkproviderserver_b.managedBlock(completablefuture::isDone);
+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
+ this.level.timings.syncChunkLoad.stopTiming(); // Paper
++ this.distanceManager.clearPriorityTickets(pair); // Paper - Chunk priority
++ this.distanceManager.clearUrgent(pair); // Paper - Chunk priority
+ } // Paper
+ ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> {
+ return ichunkaccess1;
+@@ -718,10 +742,12 @@ public class ServerChunkCache extends ChunkSource {
+ if (create && !currentlyUnloading) {
+ // CraftBukkit end
+ this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
++ if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority
+ if (this.chunkAbsent(playerchunk, l)) {
+ ProfilerFiller gameprofilerfiller = this.level.getProfiler();
+
+ gameprofilerfiller.push("chunkLoad");
++ distanceManager.delayDistanceManagerTick = false; // Paper - Chunk priority - ensure this is never false
+ this.runDistanceManagerUpdates();
+ playerchunk = this.getVisibleChunkIfPresent(k);
+ gameprofilerfiller.pop();
+@@ -731,7 +757,13 @@ public class ServerChunkCache extends ChunkSource {
+ }
+ }
+
+- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap);
++ // Paper start - Chunk priority
++ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap);
++ if (isUrgent) {
++ future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair));
++ }
++ return future;
++ // Paper end
+ }
+
+ private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
+@@ -783,6 +815,7 @@ public class ServerChunkCache extends ChunkSource {
+ }
+
+ public boolean runDistanceManagerUpdates() {
++ if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority
+ boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
+ boolean flag1 = this.chunkMap.promoteChunkMap();
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index d11b5c9e64b6695a44cc2db588bed2e8a870c607..1136a3e406b3683ba7498a7903ed32e7053ffd1d 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -184,6 +184,7 @@ public class ServerPlayer extends Player {
+ private int lastRecordedArmor = Integer.MIN_VALUE;
+ private int lastRecordedLevel = Integer.MIN_VALUE;
+ private int lastRecordedExperience = Integer.MIN_VALUE;
++ public boolean isRealPlayer; // Paper - chunk priority
+ private float lastSentHealth = -1.0E8F;
+ private int lastSentFood = -99999999;
+ private boolean lastFoodSaturationZero = true;
+@@ -327,6 +328,21 @@ public class ServerPlayer extends Player {
+ this.maxHealthCache = this.getMaxHealth();
+ this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
+ }
++ // Paper start - Chunk priority
++ public BlockPos getPointInFront(double inFront) {
++ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason
++ final double x = getX() + inFront * Math.cos(rads);
++ final double z = getZ() + inFront * Math.sin(rads);
++ return new BlockPos(x, getY(), z);
++ }
++
++ public ChunkPos getChunkInFront(double inFront) {
++ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason
++ final double x = getX() + (inFront * 16) * Math.cos(rads);
++ final double z = getZ() + (inFront * 16) * Math.sin(rads);
++ return new ChunkPos(Mth.floor(x) >> 4, Mth.floor(z) >> 4);
++ }
++ // Paper end
+
+ // Yes, this doesn't match Vanilla, but it's the best we can do for now.
+ // If this is an issue, PRs are welcome
+diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+index 43fcd179b43a2c64e6ea71da537263cc516485a0..bd475554a630fb68433dd6e6640586cf5240cfed 100644
+--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+@@ -26,15 +26,140 @@ import org.slf4j.Logger;
+ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final ProcessorMailbox<Runnable> taskMailbox;
+- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
+- private final ChunkMap chunkMap;
++ // Paper start
++ private static final int MAX_PRIORITIES = ChunkMap.MAX_CHUNK_DISTANCE + 2;
++
++ static class ChunkLightQueue {
++ public boolean shouldFastUpdate;
++ java.util.ArrayDeque<Runnable> pre = new java.util.ArrayDeque<Runnable>();
++ java.util.ArrayDeque<Runnable> post = new java.util.ArrayDeque<Runnable>();
++
++ ChunkLightQueue(long chunk) {}
++ }
++
++ static class PendingLightTask {
++ long chunkId;
++ IntSupplier priority;
++ Runnable pre;
++ Runnable post;
++ boolean fastUpdate;
++
++ public PendingLightTask(long chunkId, IntSupplier priority, Runnable pre, Runnable post, boolean fastUpdate) {
++ this.chunkId = chunkId;
++ this.priority = priority;
++ this.pre = pre;
++ this.post = post;
++ this.fastUpdate = fastUpdate;
++ }
++ }
++
++
++ // Retain the chunks priority level for queued light tasks
++ class LightQueue {
++ private int size = 0;
++ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap[MAX_PRIORITIES];
++ private final java.util.concurrent.ConcurrentLinkedQueue<PendingLightTask> pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
++ private final java.util.concurrent.ConcurrentLinkedQueue<Runnable> priorityChanges = new java.util.concurrent.ConcurrentLinkedQueue<>();
++
++ private LightQueue() {
++ for (int i = 0; i < buckets.length; i++) {
++ buckets[i] = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>();
++ }
++ }
++
++ public void changePriority(long pair, int currentPriority, int priority) {
++ this.priorityChanges.add(() -> {
++ ChunkLightQueue remove = this.buckets[currentPriority].remove(pair);
++ if (remove != null) {
++ ChunkLightQueue existing = this.buckets[Math.max(1, priority)].put(pair, remove);
++ if (existing != null) {
++ remove.pre.addAll(existing.pre);
++ remove.post.addAll(existing.post);
++ }
++ }
++ });
++ }
++
++ public final void addChunk(long chunkId, IntSupplier priority, Runnable pre, Runnable post) {
++ pendingTasks.add(new PendingLightTask(chunkId, priority, pre, post, true));
++ tryScheduleUpdate();
++ }
++
++ public final void add(long chunkId, IntSupplier priority, ThreadedLevelLightEngine.TaskType type, Runnable run) {
++ pendingTasks.add(new PendingLightTask(chunkId, priority, type == TaskType.PRE_UPDATE ? run : null, type == TaskType.POST_UPDATE ? run : null, false));
++ }
++ public final void add(PendingLightTask update) {
++ int priority = update.priority.getAsInt();
++ ChunkLightQueue lightQueue = this.buckets[priority].computeIfAbsent(update.chunkId, ChunkLightQueue::new);
++
++ if (update.pre != null) {
++ this.size++;
++ lightQueue.pre.add(update.pre);
++ }
++ if (update.post != null) {
++ this.size++;
++ lightQueue.post.add(update.post);
++ }
++ if (update.fastUpdate) {
++ lightQueue.shouldFastUpdate = true;
++ }
++ }
++
++ public final boolean isEmpty() {
++ return this.size == 0 && this.pendingTasks.isEmpty();
++ }
++
++ public final int size() {
++ return this.size;
++ }
++
++ public boolean poll(java.util.List<Runnable> pre, java.util.List<Runnable> post) {
++ PendingLightTask pending;
++ while ((pending = pendingTasks.poll()) != null) {
++ add(pending);
++ }
++ Runnable run;
++ while ((run = priorityChanges.poll()) != null) {
++ run.run();
++ }
++ boolean hasWork = false;
++ it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = this.buckets;
++ int priority = 0;
++ while (priority < MAX_PRIORITIES && !isEmpty()) {
++ it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue> bucket = buckets[priority];
++ if (bucket.isEmpty()) {
++ priority++;
++ if (hasWork) {
++ return true;
++ } else {
++ continue;
++ }
++ }
++ ChunkLightQueue queue = bucket.removeFirst();
++ this.size -= queue.pre.size() + queue.post.size();
++ pre.addAll(queue.pre);
++ post.addAll(queue.post);
++ queue.pre.clear();
++ queue.post.clear();
++ hasWork = true;
++ if (queue.shouldFastUpdate) {
++ return true;
++ }
++ }
++ return hasWork;
++ }
++ }
++
++ final LightQueue queue = new LightQueue();
++ // Paper end
++ private final ChunkMap chunkMap; private final ChunkMap playerChunkMap; // Paper
+ private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
+ private volatile int taskPerBatch = 5;
+ private final AtomicBoolean scheduled = new AtomicBoolean();
+
+ public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox<Runnable> processor, ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor) {
+ super(chunkProvider, true, hasBlockLight);
+- this.chunkMap = chunkStorage;
++ this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper
+ this.sorterMailbox = executor;
+ this.taskMailbox = processor;
+ }
+@@ -120,13 +245,9 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+ }
+
+ private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
+- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> {
+- this.lightTasks.add(Pair.of(stage, task));
+- if (this.lightTasks.size() >= this.taskPerBatch) {
+- this.runUpdate();
+- }
+-
+- }, ChunkPos.asLong(x, z), completedLevelSupplier));
++ // Paper start - replace method
++ this.queue.add(ChunkPos.asLong(x, z), completedLevelSupplier, stage, task);
++ // Paper end
+ }
+
+ @Override
+@@ -142,8 +263,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+
+ public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
+ ChunkPos chunkPos = chunk.getPos();
+- chunk.setLightCorrect(false);
+- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
++ // Paper start
++ //ichunkaccess.b(false); // Don't need to disable this
++ long pair = chunkPos.toLong();
++ CompletableFuture<ChunkAccess> future = new CompletableFuture<>();
++ IntSupplier prioritySupplier = playerChunkMap.getChunkQueueLevel(pair);
++ boolean[] skippedPre = {false};
++ this.queue.addChunk(pair, prioritySupplier, Util.name(() -> {
++ // Paper end
+ LevelChunkSection[] levelChunkSections = chunk.getSections();
+
+ for(int i = 0; i < chunk.getSectionsCount(); ++i) {
+@@ -163,51 +290,45 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+
+ }, () -> {
+ return "lightChunk " + chunkPos + " " + excludeBlocks;
+- }));
+- return CompletableFuture.supplyAsync(() -> {
++ // Paper start - merge the 2 together
++ }), () -> {
++ this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved from below, we want to call this even when returning early
++ if (skippedPre[0]) return; // Paper - future's already complete
+ chunk.setLightCorrect(true);
+ super.retainData(chunkPos, false);
+- this.chunkMap.releaseLightTicket(chunkPos);
+- return chunk;
+- }, (runnable) -> {
+- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable);
++ //this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved up
++ future.complete(chunk);
+ });
++ return future;
++ // Paper end
+ }
+
+ public void tryScheduleUpdate() {
+- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
++ if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper
+ this.taskMailbox.tell(() -> {
+ this.runUpdate();
+ this.scheduled.set(false);
++ tryScheduleUpdate(); // Paper - if we still have work to do, do it!
+ });
+ }
+
+ }
+
++ // Paper start - replace impl
++ private final java.util.List<Runnable> pre = new java.util.ArrayList<>();
++ private final java.util.List<Runnable> post = new java.util.ArrayList<>();
+ private void runUpdate() {
+- int i = Math.min(this.lightTasks.size(), this.taskPerBatch);
+- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
+-
+- int j;
+- for(j = 0; objectListIterator.hasNext() && j < i; ++j) {
+- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
+- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
+- pair.getSecond().run();
+- }
++ if (queue.poll(pre, post)) {
++ pre.forEach(Runnable::run);
++ pre.clear();
++ super.runUpdates(Integer.MAX_VALUE, true, true);
++ post.forEach(Runnable::run);
++ post.clear();
++ } else {
++ // might have level updates to go still
++ super.runUpdates(Integer.MAX_VALUE, true, true);
+ }
+-
+- objectListIterator.back(j);
+- super.runUpdates(Integer.MAX_VALUE, true, true);
+-
+- for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) {
+- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
+- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
+- pair2.getSecond().run();
+- }
+-
+- objectListIterator.remove();
+- }
+-
++ // Paper end
+ }
+
+ public void setTaskPerBatch(int taskBatchSize) {
+diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
+index f1128f0d4a9a0241ac6c9bc18dd13b431c616bb1..2b2b7851d5f68bcdb41d58bcc64740ba58bf1ef4 100644
+--- a/src/main/java/net/minecraft/server/level/Ticket.java
++++ b/src/main/java/net/minecraft/server/level/Ticket.java
+@@ -8,6 +8,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+ public final T key;
+ public long createdTick;
+ public long delayUnloadBy; // Paper
++ public int priority; // Paper - Chunk priority
+
+ protected Ticket(TicketType<T> type, int level, T argument) {
+ this.type = type;
+diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
+index 8770fe0db46b01e8b608637df4f1a669a3f4cdde..3c1698ba0d3bc412ab957777d9b5211dbc555208 100644
+--- a/src/main/java/net/minecraft/server/level/TicketType.java
++++ b/src/main/java/net/minecraft/server/level/TicketType.java
+@@ -9,6 +9,8 @@ import net.minecraft.world.level.ChunkPos;
+ public class TicketType<T> {
+ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
+ public static final TicketType<Long> ASYNC_LOAD = create("async_load", Long::compareTo); // Paper
++ public static final TicketType<ChunkPos> PRIORITY = create("priority", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper
++ public static final TicketType<ChunkPos> URGENT = create("urgent", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper
+
+ private final String name;
+ private final Comparator<T> comparator;
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index aa9d32a4c86aade43f91c78ae1179119d12e9a88..d5e5e0a80c1a3dd5823e9975062856b6d0ef3ae1 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -175,6 +175,7 @@ public abstract class PlayerList {
+ }
+
+ public void placeNewPlayer(Connection connection, ServerPlayer player) {
++ player.isRealPlayer = true; // Paper - Chunk priority
+ ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper
+ if (prev != null) {
+ disconnectPendingPlayer(prev);
+@@ -289,8 +290,8 @@ public abstract class PlayerList {
+ net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
+ net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
+ distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
+- worldserver1.getChunkSource().runDistanceManagerUpdates();
+- worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> {
++ worldserver1.getChunkSource().markAreaHighPriority(pos, 28, 3); // Paper - Chunk priority
++ worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, false).thenApply(chunk -> { // Paper - Chunk priority
+ net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pos.toLong());
+ if (updatingChunk != null) {
+ return updatingChunk.getEntityTickingChunkFuture();
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index eac485db587eba68ab0d05ac5bfd07fb20c0e766..2df33172415c8da39b9f299027cd86d2ce7c6890 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -231,7 +231,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, i
+ private BlockPos blockPosition;
+ private ChunkPos chunkPosition;
+ private Vec3 deltaMovement;
+- private float yRot;
++ public float yRot; // Paper - private->public
+ private float xRot;
+ public float yRotO;
+ public float xRotO;
+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 dc95aaa62220f2042e287c7d0d69753b8e891fba..06577d9cd276e65f2fdf5082b9ee4dc2d5211611 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -141,7 +141,7 @@ public class LevelChunk extends ChunkAccess {
+ return NEIGHBOUR_CACHE_RADIUS;
+ }
+
+- boolean loadedTicketLevel;
++ boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Paper - public accessor
+ private long neighbourChunksLoadedBitset;
+ private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)];
+
+@@ -672,6 +672,7 @@ public class LevelChunk extends ChunkAccess {
+
+ // CraftBukkit start
+ public void loadCallback() {
++ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
+ // Paper start - neighbour cache
+ int chunkX = this.chunkPos.x;
+ int chunkZ = this.chunkPos.z;
+@@ -726,6 +727,7 @@ public class LevelChunk extends ChunkAccess {
+ }
+
+ public void unloadCallback() {
++ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
+ org.bukkit.Server server = this.level.getCraftServer();
+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved());
+ server.getPluginManager().callEvent(unloadEvent);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index a5c335715a25dc154682e38a49ff308f30b41216..220db41f348437ae2d703ebae883e882804e6797 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -1992,6 +1992,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ return future;
+ }
+
++ // Paper start - Chunk priority
++ if (!urgent) {
++ // If not urgent, at least use a slightly boosted priority
++ world.getChunkSource().markHighPriority(new ChunkPos(x, z), 1);
++ }
++ // Paper end
+ return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
+ if (chunk != null) addTicket(x, z); // Paper
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index f4643f7ca1468bd7bd60637bf6096a828d1c0a40..7054a162b3899537821972cf8e53f579ee73f853 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -937,6 +937,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ throw new UnsupportedOperationException("Cannot set rotation of players. Consider teleporting instead.");
+ }
+
++ // Paper start - Chunk priority
++ @Override
++ public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location loc, @javax.annotation.Nonnull PlayerTeleportEvent.TeleportCause cause) {
++ ((CraftWorld)loc.getWorld()).getHandle().getChunkSource().markAreaHighPriority(
++ new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(loc.getX()) >> 4,
++ net.minecraft.util.Mth.floor(loc.getZ()) >> 4), 28, 3); // Load area high priority
++ return super.teleportAsync(loc, cause);
++ }
++ // Paper end
++
+ @Override
+ public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
+ Preconditions.checkArgument(location != null, "location");