diff --git a/patches/server/0053-Asynchronous-chunk-IO.patch b/patches/server/0052-Asynchronous-chunk-IO-and-loading.patch
similarity index 55%
rename from patches/server/0053-Asynchronous-chunk-IO.patch
rename to patches/server/0052-Asynchronous-chunk-IO-and-loading.patch
index f3e44b34b..c98493657 100644
--- a/patches/server/0053-Asynchronous-chunk-IO.patch
+++ b/patches/server/0052-Asynchronous-chunk-IO-and-loading.patch
@@ -1,33 +1,147 @@
-From 6a1a8b6aafe4641484ae13615a21920c863586ae Mon Sep 17 00:00:00 2001
+From da939406c7900f95e4c1888e8672968e38afe69a Mon Sep 17 00:00:00 2001
From: Spottedleaf
-Date: Tue, 9 Jul 2019 03:38:23 -0700
-Subject: [PATCH] Asynchronous chunk IO
+Date: Sat, 13 Jul 2019 09:23:10 -0700
+Subject: [PATCH] Asynchronous chunk IO and loading
---
- .../paper/io/ConcreteFileIOThread.java | 652 ++++++++++++++++++
+ .../co/aikar/timings/WorldTimingsHandler.java | 14 +
+ .../ChunkPacketBlockControllerAntiXray.java | 43 +-
+ .../paper/io/ConcreteFileIOThread.java | 661 ++++++++++++++++++
.../com/destroystokyo/paper/io/IOUtil.java | 62 ++
- .../paper/io/PrioritizedTaskQueue.java | 262 +++++++
- .../paper/io/QueueExecutorThread.java | 200 ++++++
- .../minecraft/server/ChunkProviderServer.java | 5 +
+ .../paper/io/PrioritizedTaskQueue.java | 258 +++++++
+ .../paper/io/QueueExecutorThread.java | 216 ++++++
+ .../paper/io/chunk/ChunkLoadTask.java | 104 +++
+ .../paper/io/chunk/ChunkLoadTaskManager.java | 119 ++++
+ .../minecraft/server/ChunkProviderServer.java | 135 ++++
+ .../minecraft/server/ChunkRegionLoader.java | 51 +-
+ .../net/minecraft/server/ChunkStatus.java | 1 +
+ .../net/minecraft/server/IChunkLoader.java | 29 +-
+ .../java/net/minecraft/server/MCUtil.java | 5 +
.../net/minecraft/server/MinecraftServer.java | 1 +
- .../net/minecraft/server/PlayerChunkMap.java | 152 +++-
- .../java/net/minecraft/server/RegionFile.java | 6 +-
+ .../net/minecraft/server/PlayerChunkMap.java | 204 +++++-
+ .../java/net/minecraft/server/RegionFile.java | 2 +-
.../net/minecraft/server/RegionFileCache.java | 6 +-
- .../minecraft/server/RegionFileSection.java | 57 +-
- .../net/minecraft/server/VillagePlace.java | 64 +-
- .../net/minecraft/server/WorldServer.java | 72 ++
- 12 files changed, 1501 insertions(+), 38 deletions(-)
+ .../minecraft/server/RegionFileSection.java | 56 +-
+ .../java/net/minecraft/server/TicketType.java | 1 +
+ .../net/minecraft/server/VillagePlace.java | 66 +-
+ .../net/minecraft/server/WorldServer.java | 74 ++
+ .../org/bukkit/craftbukkit/CraftWorld.java | 36 +-
+ 22 files changed, 2066 insertions(+), 78 deletions(-)
create mode 100644 src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
create mode 100644 src/main/java/com/destroystokyo/paper/io/IOUtil.java
create mode 100644 src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
create mode 100644 src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
+ create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
+ create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java
+diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+index 366de66657..7f623270f9 100644
+--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+@@ -51,6 +51,13 @@ public class WorldTimingsHandler {
+ public final Timing worldSaveChunks;
+ public final Timing worldSaveLevel;
+ public final Timing chunkSaveData;
++ public final Timing poiUnload;
++ public final Timing chunkUnload;
++ public final Timing poiSaveDataSerialization;
++ public final Timing chunkSave;
++ public final Timing chunkSaveOverwriteCheck;
++ public final Timing chunkSaveDataSerialization;
++ public final Timing chunkSaveIOWait;
+
+ public WorldTimingsHandler(World server) {
+ String name = server.worldData.getName() +" - ";
+@@ -99,6 +106,13 @@ public class WorldTimingsHandler {
+ tracker2 = Timings.ofSafe(name + "tracker stage 2");
+ doTick = Timings.ofSafe(name + "doTick");
+ tickEntities = Timings.ofSafe(name + "tickEntities");
++ poiUnload = Timings.ofSafe(name + "Chunk unload - POI");
++ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk");
++ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization");
++ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk");
++ chunkSaveOverwriteCheck = Timings.ofSafe(name + "Chunk save - Chunk Overwrite check");
++ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization");
++ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO wait");
+ }
+
+ public static Timing getTickList(WorldServer worldserver, String timingsType) {
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+index 9d8bee5cac..c2dd59f573 100644
+--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+@@ -150,6 +150,12 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
+
+ private final AtomicInteger xrayRequests = new AtomicInteger();
+
++ // Paper start - async chunk api
++ private Integer nextTicketHold() {
++ return Integer.valueOf(this.xrayRequests.getAndIncrement());
++ }
++ // Paper end
++
+ private Integer addXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider) {
+ final Integer hold = Integer.valueOf(this.xrayRequests.getAndIncrement());
+
+@@ -181,6 +187,33 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
+ chunk.world.getChunkAt(locX, locZ + 1);
+ }
+
++ // Paper start - async chunk api
++ private void loadNeighbourAsync(ChunkProviderServer chunkProvider, int chunkX, int chunkZ, int[] counter, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) {
++ chunkProvider.getChunkAtAsynchronously(chunkX, chunkZ, true, (Chunk neighbour) -> {
++ onNeighourLoad.accept(neighbour);
++ if (++counter[0] == 4) {
++ onAllNeighboursLoad.run();
++ }
++ });
++ }
++
++ private void loadNeighboursAsync(Chunk chunk, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) {
++ int[] loaded = new int[1];
++
++ int locX = chunk.getPos().x;
++ int locZ = chunk.getPos().z;
++
++ onNeighourLoad.accept(chunk);
++
++ ChunkProviderServer chunkProvider = ((WorldServer)chunk.world).getChunkProvider();
++
++ this.loadNeighbourAsync(chunkProvider, locX - 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad);
++ this.loadNeighbourAsync(chunkProvider, locX + 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad);
++ this.loadNeighbourAsync(chunkProvider, locX, locZ - 1, loaded, onNeighourLoad, onAllNeighboursLoad);
++ this.loadNeighbourAsync(chunkProvider, locX, locZ + 1, loaded, onNeighourLoad, onAllNeighboursLoad);
++ }
++ // Paper end
++
+ @Override
+ public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) {
+ int locX = chunk.getPos().x;
+@@ -256,11 +289,15 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll
+
+ if (chunks[0] == null || chunks[1] == null || chunks[2] == null || chunks[3] == null) {
+ // we need to load
+- MinecraftServer.getServer().scheduleOnMain(() -> {
+- Integer ticketHold = this.addXrayTickets(locX, locZ, world.getChunkProvider());
+- this.loadNeighbours(chunk);
++ // Paper start - async chunk api
++ Integer ticketHold = this.nextTicketHold();
++ this.loadNeighboursAsync(chunk, (Chunk neighbour) -> { // when a neighbour is loaded
++ ((WorldServer)neighbour.world).getChunkProvider().addTicket(TicketType.ANTIXRAY, neighbour.getPos(), 0, ticketHold);
++ },
++ () -> { // once neighbours get loaded
+ this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, false, ticketHold);
+ });
++ // Paper end
+ return;
+ }
+
diff --git a/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
new file mode 100644
-index 0000000000..13194d1978
+index 0000000000..8cbe522f23
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
-@@ -0,0 +1,652 @@
+@@ -0,0 +1,661 @@
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.ChunkCoordIntPair;
@@ -49,17 +163,17 @@ index 0000000000..13194d1978
+ * Prioritized singleton thread responsible for all chunk IO that occurs in a minecraft server.
+ *
+ *
-+ * Singleton access: {@link Holder#INSTANCE}
++ * Singleton access: {@link Holder#INSTANCE}
+ *
+ *
+ *
-+ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced):
-+ *
-+ * Chunk saves may not occur for unloaded chunks.
-+ *
-+ *
-+ * Tasks must be scheduled on the main thread.
-+ *
++ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced):
++ *
++ * Chunk saves may not occur for unloaded chunks.
++ *
++ *
++ * Tasks must be scheduled on the main thread.
++ *
+ *
+ *
+ * @see Holder#INSTANCE
@@ -68,7 +182,7 @@ index 0000000000..13194d1978
+ */
+public final class ConcreteFileIOThread extends QueueExecutorThread {
+
-+ private static final Logger LOGGER = MinecraftServer.LOGGER;
++ public static final Logger LOGGER = MinecraftServer.LOGGER;
+ public static final NBTTagCompound FAILURE_VALUE = new NBTTagCompound();
+
+ public static final class Holder {
@@ -83,7 +197,7 @@ index 0000000000..13194d1978
+ private final AtomicLong writeCounter = new AtomicLong();
+
+ private ConcreteFileIOThread() {
-+ super(new PrioritizedTaskQueue<>(), (int) (1.0e6)); // 1.0ms spinwait time
++ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time
+ this.setName("Concrete RegionFile IO Thread");
+ this.setPriority(Thread.NORM_PRIORITY - 1); // we keep priority close to normal because threads can wait on us
+ this.setUncaughtExceptionHandler((final Thread unused, final Throwable thr) -> {
@@ -113,10 +227,9 @@ index 0000000000..13194d1978
+
+ /**
+ * Attempts to bump the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued.
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority level to try to bump to
+ */
+ public void bumpPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
@@ -138,18 +251,17 @@ index 0000000000..13194d1978
+ }
+
+ // Hack start
-+
+ /**
+ * if {@code waitForRead} is true, then this task will wait on an available read task, else it will wait on an available
+ * write task
+ * if {@code poiTask} is true, then this task will wait on a poi task, else it will wait on chunk data task
-+ *
-+ * @return whether the task succeeded, or {@code null} if there is no task
+ * @deprecated API is garbage and will only work for main thread queueing of tasks (which is vanilla), plugins messing
-+ * around asynchronously will give unexpected results
++ * around asynchronously will give unexpected results
++ * @return whether the task succeeded, or {@code null} if there is no task
+ */
+ @Deprecated
-+ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, boolean waitForRead, boolean poiTask) {
++ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, final boolean waitForRead,
++ final boolean poiTask) {
+ final ChunkDataTask task;
+
+ final Long key = IOUtil.getCoordinateKey(chunkX, chunkZ);
@@ -180,12 +292,29 @@ index 0000000000..13194d1978
+ }
+ // Hack end
+
++ public NBTTagCompound getPendingWrite(final WorldServer world, final int chunkX, final int chunkZ, final boolean poiData) {
++ final ChunkDataController taskController = poiData ? world.poiDataController : world.chunkDataController;
++
++ final ChunkDataTask dataTask = taskController.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
++
++ if (dataTask == null) {
++ return null;
++ }
++
++ final ChunkDataController.InProgressWrite write = dataTask.inProgressWrite;
++
++ if (write == null) {
++ return null;
++ }
++
++ return write.data;
++ }
++
+ /**
+ * Sets the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued.
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority level to set to
+ */
+ public void setPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
@@ -209,24 +338,23 @@ index 0000000000..13194d1978
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ *
-+ * Impl notes:
++ * Impl notes:
+ *
+ *
-+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
-+ * saves must be scheduled before a chunk is unloaded.
++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
++ * saves must be scheduled before a chunk is unloaded.
+ *
+ *
-+ * Writes may be called concurrently, although only the "later" write will go through.
++ * Writes may be called concurrently, although only the "later" write will go through.
+ *
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param poiData Chunk point of interest data. If {@code null}, then no poi data is saved.
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param poiData Chunk point of interest data. If {@code null}, then no poi data is saved.
+ * @param chunkData Chunk data. If {@code null}, then no chunk data is saved.
-+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
++ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
+ * @throws IllegalArgumentException If both {@code poiData} and {@code chunkData} are {@code null}.
-+ * @throws IllegalStateException If the file io thread has shutdown.
++ * @throws IllegalStateException If the file io thread has shutdown.
+ */
+ public void scheduleSave(final WorldServer world, final int chunkX, final int chunkZ,
+ final NBTTagCompound poiData, final NBTTagCompound chunkData,
@@ -290,7 +418,7 @@ index 0000000000..13194d1978
+ * a {@link CompletableFuture} which is potentially completed ASYNCHRONOUSLY ON THE FILE IO THREAD when the load task
+ * has completed.
+ *
-+ * Note that if the chunk fails to load the returned future is completed with {@code null}.
++ * Note that if the chunk fails to load the returned future is completed with {@code null}.
+ *
+ */
+ public CompletableFuture loadChunkDataAsyncFuture(final WorldServer world, final int chunkX, final int chunkZ,
@@ -304,30 +432,29 @@ index 0000000000..13194d1978
+ /**
+ * Schedules a load to be executed asynchronously.
+ *
-+ * Impl notes:
++ * Impl notes:
+ *
+ *
-+ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}.
++ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}.
+ *
+ *
-+ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data
-+ * this call did not request.
++ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data
++ * this call did not request.
+ *
+ *
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
+ *
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param readPoiData Whether to read point of interest data. If {@code false}, the {@code NBTTagCompound} will be {@code null}.
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue}
++ * @param onComplete Consumer to execute once this task has completed
++ * @param readPoiData Whether to read point of interest data. If {@code false}, the {@code NBTTagCompound} will be {@code null}.
+ * @param readChunkData Whether to read chunk data. If {@code false}, the {@code NBTTagCompound} will be {@code null}.
+ * @return The {@link PrioritizedTaskQueue.PrioritizedTask} associated with this task. Note that this task does not support
-+ * cancellation.
++ * cancellation.
+ */
+ public void loadChunkDataAsync(final WorldServer world, final int chunkX, final int chunkZ,
+ final int priority, final Consumer onComplete,
@@ -342,7 +469,7 @@ index 0000000000..13194d1978
+ }
+
+ final ChunkData complete = new ChunkData();
-+ final boolean[] requireCompletion = new boolean[]{readPoiData, readChunkData};
++ final boolean[] requireCompletion = new boolean[] { readPoiData, readChunkData };
+
+ if (readPoiData) {
+ this.scheduleRead(world.poiDataController, world, chunkX, chunkZ, (final NBTTagCompound poiData) -> {
@@ -401,7 +528,7 @@ index 0000000000..13194d1978
+ // not scheduled
+
+ final Boolean shouldSchedule = intendingToBlock ? dataController.computeForRegionFile(chunkX, chunkZ, tryLoadFunction) :
-+ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction);
++ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction);
+
+ if (shouldSchedule == Boolean.FALSE) {
+ // not on disk
@@ -436,7 +563,6 @@ index 0000000000..13194d1978
+ /**
+ * Same as {@link #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns
+ * the {@link ChunkData} associated with the specified chunk when the task is complete.
-+ *
+ * @return The chunk data, or {@code null} if the chunk failed to load.
+ */
+ public ChunkData loadChunkData(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
@@ -447,7 +573,7 @@ index 0000000000..13194d1978
+ /**
+ * Schedules the given task at the specified priority to be executed on the IO thread.
+ *
-+ * Internal api. Do not use.
++ * Internal api. Do not use.
+ *
+ */
+ public void runTask(final int priority, final Runnable runnable) {
@@ -469,7 +595,7 @@ index 0000000000..13194d1978
+ this.run.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath) throwable;
++ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable);
+ }
@@ -481,8 +607,7 @@ index 0000000000..13194d1978
+ public NBTTagCompound poiData;
+ public NBTTagCompound chunkData;
+
-+ public ChunkData() {
-+ }
++ public ChunkData() {}
+
+ public ChunkData(final NBTTagCompound poiData, final NBTTagCompound chunkData) {
+ this.poiData = poiData;
@@ -496,11 +621,9 @@ index 0000000000..13194d1978
+ public final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f);
+
+ public abstract void writeData(final int x, final int z, final NBTTagCompound compound) throws IOException;
-+
+ public abstract NBTTagCompound readData(final int x, final int z) throws IOException;
+
+ public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function);
-+
+ public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function);
+
+ public static final class InProgressWrite {
@@ -576,7 +699,7 @@ index 0000000000..13194d1978
+ compound = this.taskController.readData(this.x, this.z);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath) thr;
++ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to read chunk data for (" + this.x + "," + this.z + ")", thr);
+ // fall through to complete with null data
@@ -619,7 +742,7 @@ index 0000000000..13194d1978
+ return;
+ }
+
-+ for (; ; ) {
++ for (;;) {
+ final long writeCounter;
+ final NBTTagCompound data;
+
@@ -635,14 +758,14 @@ index 0000000000..13194d1978
+ this.taskController.writeData(this.x, this.z, data);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath) thr;
++ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to write chunk data for (" + this.x + "," + this.z + ")", thr);
+ failedWrite = true;
+ }
+
+ boolean finalFailWrite = failedWrite;
-+ boolean[] returnFailWrite = new boolean[]{false};
++ boolean[] returnFailWrite = new boolean[] { false };
+
+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
@@ -682,7 +805,7 @@ index 0000000000..13194d1978
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/IOUtil.java b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
new file mode 100644
-index 0000000000..7898cab62f
+index 0000000000..5af0ac3d9e
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
@@ -0,0 +1,62 @@
@@ -695,15 +818,15 @@ index 0000000000..7898cab62f
+ /* Copied from concrete or concurrentutil */
+
+ public static long getCoordinateKey(final int x, final int z) {
-+ return ((long) z << 32) | (x & 0xFFFFFFFFL);
++ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public static int getCoordinateX(final long key) {
-+ return (int) key;
++ return (int)key;
+ }
+
+ public static int getCoordinateZ(final long key) {
-+ return (int) (key >>> 32);
++ return (int)(key >>> 32);
+ }
+
+ public static int getRegionCoordinate(final int chunkCoordinate) {
@@ -744,17 +867,16 @@ index 0000000000..7898cab62f
+
+ @SuppressWarnings("unchecked")
+ public static void rethrow(final Throwable throwable) throws T {
-+ throw (T) throwable;
++ throw (T)throwable;
+ }
+
+}
-\ No newline at end of file
diff --git a/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
new file mode 100644
-index 0000000000..3722991e1b
+index 0000000000..c3ca3c4a1c
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
-@@ -0,0 +1,262 @@
+@@ -0,0 +1,258 @@
+package com.destroystokyo.paper.io;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
@@ -770,42 +892,42 @@ index 0000000000..3722991e1b
+ /**
+ * Priority value indicating the task has completed or is being completed.
+ */
-+ public static final int COMPLETING_PRIORITY = -1;
++ public static final int COMPLETING_PRIORITY = -1;
+
+ /**
+ * Highest priority, should only be used for main thread tasks or tasks that are blocking the main thread.
+ */
-+ public static final int HIGHEST_PRIORITY = 0;
++ public static final int HIGHEST_PRIORITY = 0;
+
+ /**
+ * Should be only used in an IO task so that chunk loads do not wait on other IO tasks.
+ * This only exists because IO tasks are scheduled before chunk load tasks to decrease IO waiting times.
+ */
-+ public static final int HIGHER_PRIORITY = 1;
++ public static final int HIGHER_PRIORITY = 1;
+
+ /**
+ * Should be used for scheduling chunk loads/generation that would increase response times to users.
+ */
-+ public static final int HIGH_PRIORITY = 2;
++ public static final int HIGH_PRIORITY = 2;
+
+ /**
+ * Default priority.
+ */
-+ public static final int NORMAL_PRIORITY = 3;
++ public static final int NORMAL_PRIORITY = 3;
+
+ /**
+ * Use for tasks not at all critical and can potentially be delayed.
+ */
-+ public static final int LOW_PRIORITY = 4;
++ public static final int LOW_PRIORITY = 4;
+
+ /**
+ * Use for tasks that should "eventually" execute.
+ */
-+ public static final int LOWEST_PRIORITY = 5;
++ public static final int LOWEST_PRIORITY = 5;
+
-+ private static final int TOTAL_PRIORITIES = 6;
++ private static final int TOTAL_PRIORITIES = 6;
+
-+ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[]) new ConcurrentLinkedQueue[TOTAL_PRIORITIES];
++ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[])new ConcurrentLinkedQueue[TOTAL_PRIORITIES];
+
+ private final AtomicBoolean shutdown = new AtomicBoolean();
+
@@ -824,7 +946,6 @@ index 0000000000..3722991e1b
+
+ /**
+ * Queues a task.
-+ *
+ * @throws IllegalStateException If the task has already been queued. Use {@link PrioritizedTask#raisePriority(int)} to
+ * raise a task's priority.
+ * This can also be thrown if the queue has shutdown.
@@ -862,12 +983,11 @@ index 0000000000..3722991e1b
+ * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
+ * result in {@link IllegalStateException} being thrown.
+ *
-+ * This operation is atomic with respect to other shutdown calls
++ * This operation is atomic with respect to other shutdown calls
+ *
+ *
-+ * After this call has completed, regardless of return value, this queue will be shutdown.
++ * After this call has completed, regardless of return value, this queue will be shutdown.
+ *
-+ *
+ * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
+ */
+ public boolean shutdown() {
@@ -899,8 +1019,15 @@ index 0000000000..3722991e1b
+ return this.priority.get();
+ }
+
++ /**
++ * Returns whether this task is scheduled to execute, or has been already executed.
++ */
++ public boolean isScheduled() {
++ return this.queue.get() != null;
++ }
++
+ final int tryComplete(final int minPriority) {
-+ for (int curr = this.getPriorityVolatile(); ; ) {
++ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return COMPLETING_PRIORITY;
+ }
@@ -918,7 +1045,6 @@ index 0000000000..3722991e1b
+
+ /**
+ * Forces this task to be completed.
-+ *
+ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
+ */
+ public boolean cancel() {
@@ -927,7 +1053,6 @@ index 0000000000..3722991e1b
+
+ /**
+ * Attempts to raise the priority to the priority level specified.
-+ *
+ * @param priority Priority specified
+ * @return {@code true} if successful, {@code false} otherwise.
+ */
@@ -936,13 +1061,7 @@ index 0000000000..3722991e1b
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
-+ final PrioritizedTaskQueue queue = this.queue.get();
-+
-+ if (queue == null) {
-+ throw new IllegalStateException("Not queued");
-+ }
-+
-+ for (int curr = this.getPriorityVolatile(); ; ) {
++ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
@@ -951,8 +1070,11 @@ index 0000000000..3722991e1b
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
-+ //noinspection unchecked
-+ queue.queues[priority].add(this); // silently fail on shutdown
++ PrioritizedTaskQueue queue = this.queue.get();
++ if (queue != null) {
++ //noinspection unchecked
++ queue.queues[priority].add(this); // silently fail on shutdown
++ }
+ return true;
+ }
+ continue;
@@ -961,7 +1083,6 @@ index 0000000000..3722991e1b
+
+ /**
+ * Attempts to set this task's priority level to the level specified.
-+ *
+ * @param priority Specified priority level.
+ * @return {@code true} if successful, {@code false} if this task is completing or has completed.
+ */
@@ -970,13 +1091,7 @@ index 0000000000..3722991e1b
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
-+ final PrioritizedTaskQueue queue = this.queue.get();
-+
-+ if (queue == null) {
-+ throw new IllegalStateException("Not queued");
-+ }
-+
-+ for (int curr = this.getPriorityVolatile(); ; ) {
++ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
@@ -985,8 +1100,11 @@ index 0000000000..3722991e1b
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
-+ //noinspection unchecked
-+ queue.queues[priority].add(this); // silently fail on shutdown
++ PrioritizedTaskQueue queue = this.queue.get();
++ if (queue != null) {
++ //noinspection unchecked
++ queue.queues[priority].add(this); // silently fail on shutdown
++ }
+ return true;
+ }
+ continue;
@@ -1017,13 +1135,12 @@ index 0000000000..3722991e1b
+ }
+ }
+}
-\ No newline at end of file
diff --git a/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
new file mode 100644
-index 0000000000..1b64cec885
+index 0000000000..609b2038f2
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
-@@ -0,0 +1,200 @@
+@@ -0,0 +1,216 @@
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.MinecraftServer;
@@ -1047,7 +1164,7 @@ index 0000000000..1b64cec885
+ protected volatile int flushCounter;
+
+ public QueueExecutorThread(final PrioritizedTaskQueue queue) {
-+ this(queue, (int) (1.e6)); // 1.0ms
++ this(queue, (int)(1.e6)); // 1.0ms
+ }
+
+ public QueueExecutorThread(final PrioritizedTaskQueue queue, final long spinWaitTime) { // in ms
@@ -1059,14 +1176,14 @@ index 0000000000..1b64cec885
+ public void run() {
+ final long spinWaitTime = this.spinWaitTime;
+ main_loop:
-+ for (; ; ) {
++ for (;;) {
+ this.pollTasks();
+
+ // spinwait
+
+ final long start = System.nanoTime();
+
-+ for (; ; ) {
++ for (;;) {
+ // If we are interrpted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
+ Thread.interrupted();
+ LockSupport.parkNanos("Spinwaiting on tasks", 1000L); // 1us
@@ -1116,13 +1233,13 @@ index 0000000000..1b64cec885
+ Runnable task;
+ boolean ret = false;
+
-+ while ((task = (Runnable) this.queue.poll()) != null) {
++ while ((task = (Runnable)this.queue.poll()) != null) {
+ ret = true;
+ try {
+ task.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath) throwable;
++ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Exception thrown from prioritized runnable task in thread " + this.getName() + ": " + IOUtil.genericToString(task), throwable);
+ }
@@ -1145,13 +1262,30 @@ index 0000000000..1b64cec885
+
+ }
+
-+ protected void queueTask(final T task) {
-+ this.queue.add(task);
++ public boolean isWaitingForTasks() {
++ return this.parked.get();
++ }
++
++ public boolean notifyTasks() {
++ if (this.parked.get() && this.parked.getAndSet(false)) {
++ LockSupport.unpark(this);
++ return true;
++ }
++ return false;
++ }
++
++ public void onTaskQueue() {
+ if (this.parked.get() && this.parked.getAndSet(false)) {
+ LockSupport.unpark(this);
+ }
+ }
+
++ protected void queueTask(final T task) {
++ this.queue.add(task);
++ this.onTaskQueue();
++ }
++
++
+ /**
+ * Waits until this thread's queue is empty.
+ *
@@ -1206,12 +1340,11 @@ index 0000000000..1b64cec885
+ /**
+ * Closes this queue executor's queue and optionally waits for it to empty.
+ *
-+ * If wait is {@code true}, then the queue will be empty by the time this call completes.
++ * If wait is {@code true}, then the queue will be empty by the time this call completes.
+ *
+ *
-+ * This function is MT-Safe.
++ * This function is MT-Safe.
+ *
-+ *
+ * @param wait If this call is to wait until the queue is empty
+ * @return whether this thread shut down the queue
+ */
@@ -1224,12 +1357,382 @@ index 0000000000..1b64cec885
+ return ret;
+ }
+}
-\ No newline at end of file
+diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
+new file mode 100644
+index 0000000000..566d1684a5
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
+@@ -0,0 +1,104 @@
++package com.destroystokyo.paper.io.chunk;
++
++import com.destroystokyo.paper.io.ConcreteFileIOThread;
++import com.destroystokyo.paper.io.IOUtil;
++import com.destroystokyo.paper.io.PrioritizedTaskQueue;
++import net.minecraft.server.ChunkCoordIntPair;
++import net.minecraft.server.ChunkRegionLoader;
++import net.minecraft.server.PlayerChunkMap;
++import net.minecraft.server.WorldServer;
++
++import java.io.IOException;
++import java.util.ArrayDeque;
++import java.util.function.Consumer;
++
++public class ChunkLoadTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
++
++ public final WorldServer world;
++ public final int chunkX;
++ public final int chunkZ;
++ public final ChunkLoadTaskManager taskManager;
++
++ final Consumer onComplete;
++
++ public ConcreteFileIOThread.ChunkData chunkData;
++
++ public ChunkLoadTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
++ final Consumer onComplete,
++ final ChunkLoadTaskManager taskManager) {
++ super(priority);
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.taskManager = taskManager;
++ this.onComplete = onComplete;
++ }
++
++ private static final ArrayDeque EMPTY_QUEUE = new ArrayDeque<>();
++
++ private static ChunkRegionLoader.InProgressChunkHolder createEmptyHolder() {
++ return new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE);
++ }
++
++ @Override
++ public void run() {
++ // either executed synchronously or asynchronously
++ final ConcreteFileIOThread.ChunkData chunkData = this.chunkData;
++
++ if (chunkData.poiData == ConcreteFileIOThread.FAILURE_VALUE || chunkData.chunkData == ConcreteFileIOThread.FAILURE_VALUE) {
++ ConcreteFileIOThread.LOGGER.error("Could not load chunk at (" + this.chunkX + "," + this.chunkZ + ")");
++ this.complete(ChunkLoadTask.createEmptyHolder());
++ return;
++ }
++
++ if (chunkData.chunkData == null) {
++ ChunkRegionLoader.InProgressChunkHolder ret = new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE);
++ ret.poiData = chunkData.poiData;
++ this.complete(ret);
++ return;
++ }
++
++ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(this.chunkX, this.chunkZ);
++
++ final PlayerChunkMap chunkManager = this.world.getChunkProvider().playerChunkMap;
++
++ final ChunkRegionLoader.InProgressChunkHolder chunkHolder = ChunkRegionLoader.loadChunk(this.world,
++ chunkManager.definedStructureManager, chunkManager.getVillagePlace(), chunkPos,
++ chunkData.chunkData, true);
++
++ // apply fixes
++
++ try {
++ chunkData.chunkData = chunkManager.getChunkData(this.world.getWorldProvider().getDimensionManager(),
++ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData, chunkPos, this.world);
++ } catch (IOException ex) {
++ ConcreteFileIOThread.LOGGER.error("Could not apply datafixers for chunk at (" + this.chunkX + "," + this.chunkZ + ")", ex);
++ this.complete(ChunkLoadTask.createEmptyHolder());
++ }
++
++ this.complete(chunkHolder);
++ }
++
++ private void complete(final ChunkRegionLoader.InProgressChunkHolder holder) {
++ this.onComplete.accept(holder);
++ this.taskManager.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
++ if (valueInMap != ChunkLoadTask.this) {
++ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other:" + valueInMap);
++ }
++ return null;
++ });
++ }
++
++ @Override
++ public boolean raisePriority(final int priority) {
++ ConcreteFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority);
++ return super.raisePriority(priority);
++ }
++
++ @Override
++ public boolean updatePriority(final int priority) {
++ ConcreteFileIOThread.Holder.INSTANCE.setPriority(this.world, this.chunkX, this.chunkZ, priority);
++ return super.updatePriority(priority);
++ }
++
++}
+diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java
+new file mode 100644
+index 0000000000..8dbaaba3cd
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java
+@@ -0,0 +1,119 @@
++package com.destroystokyo.paper.io.chunk;
++
++import com.destroystokyo.paper.io.ConcreteFileIOThread;
++import com.destroystokyo.paper.io.IOUtil;
++import com.destroystokyo.paper.io.PrioritizedTaskQueue;
++import com.destroystokyo.paper.io.QueueExecutorThread;
++import net.minecraft.server.ChunkRegionLoader;
++import net.minecraft.server.IAsyncTaskHandler;
++import net.minecraft.server.WorldServer;
++import org.spigotmc.AsyncCatcher;
++
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.function.Consumer;
++
++public final class ChunkLoadTaskManager {
++
++ private final QueueExecutorThread[] workers;
++ private final WorldServer world;
++
++ private final PrioritizedTaskQueue queue = new PrioritizedTaskQueue<>();
++
++ final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f);
++
++ public ChunkLoadTaskManager(final WorldServer world, final int threads) {
++ this.world = world;
++ this.workers = threads <= 0 ? null : new QueueExecutorThread[threads];
++
++ for (int i = 0; i < threads; ++i) {
++ this.workers[i] = new QueueExecutorThread<>(this.queue, (long)0.10e6); //0.1ms
++ this.workers[i].setName("Async chunk loader thread for world: " + world.getWorldData().getName());
++ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1);
++ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
++ ConcreteFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
++ });
++ this.workers[i].start();
++ }
++
++ }
++
++ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
++ final Consumer onComplete,
++ final boolean intendingToBlock) {
++ AsyncCatcher.catchOp("Async chunk load schedule");
++ final WorldServer world = this.world;
++
++ return this.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
++ if (valueInMap != null) {
++ throw new IllegalStateException("Double scheduling chunk load");
++ }
++
++ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, onComplete, ChunkLoadTaskManager.this);
++
++ ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final ConcreteFileIOThread.ChunkData chunkData) -> {
++ ret.chunkData = chunkData;
++ ChunkLoadTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
++ }, true, true, intendingToBlock);
++
++ return ret;
++ });
++ }
++
++ public void flush() {
++ ConcreteFileIOThread.Holder.INSTANCE.flush();
++ if (this.workers == null) {
++ return;
++ }
++ for (final QueueExecutorThread worker : this.workers) {
++ worker.flush();
++ }
++ }
++
++ public void shutdown(final boolean wait) {
++ if (wait) {
++ ConcreteFileIOThread.Holder.INSTANCE.flush();
++ }
++
++ if (this.workers == null) {
++ return;
++ }
++
++ for (final QueueExecutorThread worker : this.workers) {
++ worker.close(false);
++ }
++
++ if (wait) {
++ this.flush();
++ }
++ }
++
++ public void raisePriority(final int chunkX, final int chunkZ, final int priority) {
++ ChunkLoadTask task = this.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
++ if (task != null) {
++ task.raisePriority(priority);
++ this.internalScheduleNotify();
++ }
++ }
++
++ protected void internalSchedule(final ChunkLoadTask task) {
++ if (this.workers == null) {
++ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).addTask(task);
++ return;
++ }
++
++ // It's important we order the task to be executed before notifying.
++ this.queue.add(task);
++ if (task.isScheduled()) {
++ this.internalScheduleNotify();
++ }
++ }
++
++ protected void internalScheduleNotify() {
++ for (final QueueExecutorThread worker : this.workers) {
++ if (worker.notifyTasks()) {
++ break;
++ }
++ }
++ }
++
++}
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-index b46285ecdc..f6a6421140 100644
+index db9113994e..4f7c442264 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-@@ -286,6 +286,7 @@ public class ChunkProviderServer extends IChunkProvider {
+@@ -147,11 +147,143 @@ public class ChunkProviderServer extends IChunkProvider {
+ return playerChunk.getAvailableChunkNow();
+
+ }
++
++ private long asyncLoadSeqCounter;
++
++ public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) {
++ if (Thread.currentThread() != this.serverThread) {
++ this.serverThreadQueue.execute(() -> {
++ this.getChunkAtAsynchronously(x, z, gen, onComplete);
++ });
++ return;
++ }
++
++ long k = ChunkCoordIntPair.pair(x, z);
++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z);
++
++ IChunkAccess ichunkaccess;
++
++ // try cache
++ for (int l = 0; l < 4; ++l) {
++ if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) {
++ ichunkaccess = this.cacheChunk[l];
++ if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
++
++ // move to first in cache
++
++ for (int i1 = 3; i1 > 0; --i1) {
++ this.cachePos[i1] = this.cachePos[i1 - 1];
++ this.cacheStatus[i1] = this.cacheStatus[i1 - 1];
++ this.cacheChunk[i1] = this.cacheChunk[i1 - 1];
++ }
++
++ this.cachePos[0] = k;
++ this.cacheStatus[0] = ChunkStatus.FULL;
++ this.cacheChunk[0] = ichunkaccess;
++
++ onComplete.accept((Chunk)ichunkaccess);
++
++ return;
++ }
++ }
++ }
++
++ if (gen) {
++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
++ return;
++ }
++
++ IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions
++ if (current != null) {
++ if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) {
++ onComplete.accept(null); // the chunk is not gen'd
++ return;
++ }
++ // we know the chunk is at full status here (either in read-only mode or the real thing)
++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
++ return;
++ } else {
++ // Paper start - async io
++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); // Paper - async io - move to own method
++
++ if (status == ChunkStatus.EMPTY) {
++ // does not exist on disk
++ onComplete.accept(null);
++ return;
++ }
++
++ if (status == ChunkStatus.FULL) {
++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
++ return;
++ } else if (status != null) {
++ onComplete.accept(null);
++ return; // not full status on disk
++ }
++ // status is null here
++ // Paper end
++
++ // at this stage we don't know what status the chunk is in
++ }
++
++ // here we don't know what status it is and we're not supposed to generate
++ // so we asynchronously load empty status
++
++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> {
++ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) {
++ // the chunk on disk was not a full status chunk
++ onComplete.accept(null);
++ return;
++ }
++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required
++ });
++ }
++
++ private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) {
++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete);
++ }
++
++ private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) {
++ CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true);
++ long identifier = this.asyncLoadSeqCounter++;
++ int ticketLevel = MCUtil.getTicketLevelFor(status);
++ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
++
++ future.whenCompleteAsync((Either either, Throwable throwable) -> {
++ // either left -> success
++ // either right -> failure
++
++ if (throwable != null) {
++ throw new RuntimeException(throwable);
++ }
++
++ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
++ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading
++
++ Optional failure = either.right();
++
++ if (failure.isPresent()) {
++ // failure
++ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString());
++ }
++
++ onComplete.accept(either.left().get());
++
++ }, this.serverThreadQueue);
++ }
++
++ public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
++ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
++ }
++
++ public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
++ this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
++ }
+ // Paper end
+
@Nullable
@Override
public IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag) {
@@ -1237,17 +1740,224 @@ index b46285ecdc..f6a6421140 100644
if (Thread.currentThread() != this.serverThread) {
return (IChunkAccess) CompletableFuture.supplyAsync(() -> {
return this.getChunkAt(i, j, chunkstatus, flag);
-@@ -307,6 +308,10 @@ public class ChunkProviderServer extends IChunkProvider {
+@@ -173,6 +305,9 @@ public class ChunkProviderServer extends IChunkProvider {
CompletableFuture> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag);
if (!completablefuture.isDone()) { // Paper
-+ // Paper start - async chunk io
-+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.bumpPriority(this.world, x, z,
-+ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
++ // Paper start - async chunk io // Paper start - async chunk loading
++ this.world.asyncLoadManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
+ // Paper end
this.world.timings.chunkAwait.startTiming(); // Paper
this.serverThreadQueue.awaitTasks(completablefuture::isDone);
this.world.timings.chunkAwait.stopTiming(); // Paper
+diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+index f88e3d957f..bee1330763 100644
+--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
++++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+@@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+ import it.unimi.dsi.fastutil.longs.LongSet;
+ import it.unimi.dsi.fastutil.shorts.ShortList;
+ import it.unimi.dsi.fastutil.shorts.ShortListIterator;
++import java.util.ArrayDeque; // Paper
+ import java.util.Arrays;
+ import java.util.BitSet;
+ import java.util.EnumSet;
+@@ -22,7 +23,29 @@ public class ChunkRegionLoader {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
++ // Paper start
++ public static final class InProgressChunkHolder {
++
++ public final ProtoChunk protoChunk;
++ public final ArrayDeque tasks;
++
++ public NBTTagCompound poiData;
++
++ public InProgressChunkHolder(final ProtoChunk protoChunk, final ArrayDeque tasks) {
++ this.protoChunk = protoChunk;
++ this.tasks = tasks;
++ }
++ }
++
+ public static ProtoChunk loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) {
++ InProgressChunkHolder holder = loadChunk(worldserver, definedstructuremanager, villageplace, chunkcoordintpair, nbttagcompound, true);
++ holder.tasks.forEach(Runnable::run);
++ return holder.protoChunk;
++ }
++
++ public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) {
++ ArrayDeque tasksToExecuteOnMain = new ArrayDeque<>();
++ // Paper end
+ ChunkGenerator> chunkgenerator = worldserver.getChunkProvider().getChunkGenerator();
+ WorldChunkManager worldchunkmanager = chunkgenerator.getWorldChunkManager();
+ NBTTagCompound nbttagcompound1 = nbttagcompound.getCompound("Level");
+@@ -66,7 +89,9 @@ public class ChunkRegionLoader {
+ LightEngine lightengine = chunkproviderserver.getLightEngine();
+
+ if (flag) {
+- lightengine.b(chunkcoordintpair, true);
++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
++ lightengine.b(chunkcoordintpair, true);
++ }); // Paper - delay this task since we're executing off-main
+ }
+
+ for (int k = 0; k < nbttaglist.size(); ++k) {
+@@ -82,16 +107,30 @@ public class ChunkRegionLoader {
+ achunksection[b0] = chunksection;
+ }
+
+- villageplace.a(chunkcoordintpair, chunksection);
++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
++ villageplace.a(chunkcoordintpair, chunksection);
++ }); // Paper - delay this task since we're executing off-main
+ }
+
+ if (flag) {
+ if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) {
+- lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("BlockLight")));
++ // Paper start - delay this task since we're executing off-main
++ NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight"));
++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified
++ tasksToExecuteOnMain.add(() -> {
++ lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), blockLight);
++ });
++ // Paper end
+ }
+
+ if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) {
+- lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("SkyLight")));
++ // Paper start - delay this task since we're executing off-main
++ NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight"));
++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified
++ tasksToExecuteOnMain.add(() -> {
++ lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight);
++ });
++ // Paper end
+ }
+ }
+ }
+@@ -194,7 +233,7 @@ public class ChunkRegionLoader {
+ }
+
+ if (chunkstatus_type == ChunkStatus.Type.LEVELCHUNK) {
+- return new ProtoChunkExtension((Chunk) object);
++ return new InProgressChunkHolder(new ProtoChunkExtension((Chunk) object), tasksToExecuteOnMain); // Paper - Async chunk loading
+ } else {
+ ProtoChunk protochunk1 = (ProtoChunk) object;
+
+@@ -233,7 +272,7 @@ public class ChunkRegionLoader {
+ protochunk1.a(worldgenstage_features, BitSet.valueOf(nbttagcompound5.getByteArray(s1)));
+ }
+
+- return protochunk1;
++ return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading
+ }
+ }
+
+diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java
+index e324989b46..abb0d69d2f 100644
+--- a/src/main/java/net/minecraft/server/ChunkStatus.java
++++ b/src/main/java/net/minecraft/server/ChunkStatus.java
+@@ -153,6 +153,7 @@ public class ChunkStatus {
+ return ChunkStatus.q.size();
+ }
+
++ public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER
+ public static int a(ChunkStatus chunkstatus) {
+ return ChunkStatus.r.getInt(chunkstatus.c());
+ }
+diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java
+index 3f14392e6e..cc933ec067 100644
+--- a/src/main/java/net/minecraft/server/IChunkLoader.java
++++ b/src/main/java/net/minecraft/server/IChunkLoader.java
+@@ -3,6 +3,10 @@ package net.minecraft.server;
+ import com.mojang.datafixers.DataFixer;
+ import java.io.File;
+ import java.io.IOException;
++// Paper start
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.CompletionException;
++// Paper end
+ import java.util.function.Supplier;
+ import javax.annotation.Nullable;
+
+@@ -10,7 +14,9 @@ public class IChunkLoader extends RegionFileCache {
+
+ protected final DataFixer b;
+ @Nullable
+- private PersistentStructureLegacy a;
++ private volatile PersistentStructureLegacy a; // Paper - async chunk loading
++
++ private final Object persistentDataLock = new Object(); // Paper
+
+ public IChunkLoader(File file, DataFixer datafixer) {
+ super(file);
+@@ -55,9 +61,26 @@ public class IChunkLoader extends RegionFileCache {
+ NBTTagCompound level = nbttagcompound.getCompound("Level");
+ if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
+ ChunkProviderServer cps = (generatoraccess == null) ? null : ((WorldServer) generatoraccess).getChunkProvider();
++ // Paper start - Async chunk loading
++ CompletableFuture future = new CompletableFuture<>();
++ MCUtil.ensureMain((Runnable)() -> {
++ try {
++ // Paper end
+ if (check(cps, pos.x - 1, pos.z) && check(cps, pos.x - 1, pos.z - 1) && check(cps, pos.x, pos.z - 1)) {
+ level.setBoolean("LightPopulated", true);
+ }
++ // Paper start - Async chunk loading
++ future.complete(null);
++ } catch (IOException ex) {
++ future.completeExceptionally(ex);
++ }
++ });
++ try {
++ future.join();
++ } catch (CompletionException ex) {
++ com.destroystokyo.paper.util.SneakyThrow.sneaky(ex.getCause());
++ }
++ // Paper end
+ }
+ }
+ // CraftBukkit end
+@@ -65,11 +88,13 @@ public class IChunkLoader extends RegionFileCache {
+ if (i < 1493) {
+ nbttagcompound = GameProfileSerializer.a(this.b, DataFixTypes.CHUNK, nbttagcompound, i, 1493);
+ if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
+ if (this.a == null) {
+ this.a = PersistentStructureLegacy.a(dimensionmanager.getType(), (WorldPersistentData) supplier.get()); // CraftBukkit - getType
+ }
+
+ nbttagcompound = this.a.a(nbttagcompound);
++ } // Paper - Async chunk loading
+ }
+ }
+
+@@ -89,7 +114,9 @@ public class IChunkLoader extends RegionFileCache {
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException {
+ super.write(chunkcoordintpair, nbttagcompound);
+ if (this.a != null) {
++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
+ this.a.a(chunkcoordintpair.pair());
++ } // Paper - Async chunk loading
+ }
+
+ }
+diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
+index 23d1935dd5..14f8b61042 100644
+--- a/src/main/java/net/minecraft/server/MCUtil.java
++++ b/src/main/java/net/minecraft/server/MCUtil.java
+@@ -530,4 +530,9 @@ public final class MCUtil {
+ out.print(fileData);
+ }
+ }
++
++ public static int getTicketLevelFor(ChunkStatus status) {
++ // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean)
++ return 33 + ChunkStatus.getTicketLevelOffset(status);
++ }
+ }
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 0324a90ca5..430cd70cf5 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -1261,7 +1971,7 @@ index 0324a90ca5..430cd70cf5 100644
public String getServerIp() {
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
-index a439277813..6ca98b7ad5 100644
+index a439277813..f25ca782b9 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -57,7 +57,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
@@ -1269,10 +1979,19 @@ index a439277813..6ca98b7ad5 100644
private final IAsyncTaskHandler executor;
public final ChunkGenerator> chunkGenerator;
- private final Supplier m;
-+ private final Supplier m; private final Supplier getWorldPersistentDataSupplier() { return this.m; } // Paper - OBFHELPER
++ private final Supplier m; public final Supplier getWorldPersistentDataSupplier() { return this.m; } // Paper - OBFHELPER
private final VillagePlace n;
public final LongSet unloadQueue;
private boolean updatingChunksModified;
+@@ -67,7 +67,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+ public final WorldLoadListener worldLoadListener;
+ public final PlayerChunkMap.a u; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.u; } // Paper - OBFHELPER // CraftBukkit - private -> public // PAIL chunkDistanceManager
+ private final AtomicInteger v;
+- private final DefinedStructureManager definedStructureManager;
++ public final DefinedStructureManager definedStructureManager; // Paper - private -> public
+ private final File x;
+ private final PlayerMap playerMap;
+ public final Int2ObjectMap trackedEntities;
@@ -101,7 +101,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
this.lightEngine = new LightEngineThreaded(ilightaccess, this, this.world.getWorldProvider().g(), threadedmailbox2, this.q.a(threadedmailbox2, false));
this.u = new PlayerChunkMap.a(executor, iasynctaskhandler);
@@ -1286,7 +2005,7 @@ index a439277813..6ca98b7ad5 100644
@Override
public void close() throws IOException {
this.q.close();
-+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.flush(); // Paper - Required since we're closing regionfiles in the next line
++ this.world.asyncLoadManager.shutdown(true); // Paper - Required since we're closing regionfiles in the next line
this.n.close();
super.close();
}
@@ -1299,7 +2018,23 @@ index a439277813..6ca98b7ad5 100644
++savedThisTick;
playerchunk.m();
}
-@@ -386,7 +387,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+@@ -328,11 +329,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+ protected void unloadChunks(BooleanSupplier booleansupplier) {
+ GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
+
++ try (Timing ignored = this.world.timings.poiUnload.startTiming()) { // Paper
+ gameprofilerfiller.enter("poi");
+ this.n.a(booleansupplier);
++ }
+ gameprofilerfiller.exitEnter("chunk_unload");
+ if (!this.world.isSavingDisabled()) {
++ try (Timing ignored = this.world.timings.chunkUnload.startTiming()) { // Paper
+ this.b(booleansupplier);
++ }
+ }
+
+ gameprofilerfiller.exit();
+@@ -386,7 +391,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
((Chunk) ichunkaccess).setLoaded(false);
}
@@ -1308,7 +2043,7 @@ index a439277813..6ca98b7ad5 100644
if (this.loadedChunks.remove(i) && ichunkaccess instanceof Chunk) {
Chunk chunk = (Chunk) ichunkaccess;
-@@ -462,13 +463,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+@@ -462,26 +467,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
}
@@ -1321,7 +2056,7 @@ index a439277813..6ca98b7ad5 100644
private CompletableFuture> f(ChunkCoordIntPair chunkcoordintpair) {
- return CompletableFuture.supplyAsync(() -> {
+ // Paper start - Async chunk io
-+ final java.util.function.BiFunction> callable = (chunkData, ioThrowable) -> {
++ final java.util.function.BiFunction> callable = (chunkHolder, ioThrowable) -> {
try (Timing ignored = this.world.timings.syncChunkLoadTimer.startTimingIfSync()) { // Paper
- NBTTagCompound nbttagcompound; // Paper
- try (Timing ignored2 = this.world.timings.chunkIOStage1.startTimingIfSync()) { // Paper
@@ -1329,13 +2064,32 @@ index a439277813..6ca98b7ad5 100644
+ if (ioThrowable != null) {
+ com.destroystokyo.paper.io.IOUtil.rethrow(ioThrowable);
}
-+ this.n.loadInData(chunkcoordintpair, chunkData.poiData);
-+ NBTTagCompound nbttagcompound = this.completeChunkData(chunkData.chunkData, chunkcoordintpair);
++ this.getVillagePlace().loadInData(chunkcoordintpair, chunkHolder.poiData);
++ chunkHolder.tasks.forEach(Runnable::run);
++ // Paper - async load completes this
+ // Paper end
- if (nbttagcompound != null) {
- boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8);
-@@ -495,7 +505,24 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+- if (nbttagcompound != null) {
+- boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8);
+-
+- if (flag) {
+- ProtoChunk protochunk = ChunkRegionLoader.loadChunk(this.world, this.definedStructureManager, this.n, chunkcoordintpair, nbttagcompound);
+-
+- protochunk.setLastSaved(this.world.getTime());
+- return Either.left(protochunk);
+- }
+-
+- PlayerChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", chunkcoordintpair);
++ // Paper start - This is done async
++ if (chunkHolder.protoChunk != null) {
++ chunkHolder.protoChunk.setLastSaved(this.world.getTime());
++ return Either.left(chunkHolder.protoChunk);
+ }
++ // Paper end
+ } catch (ReportedException reportedexception) {
+ Throwable throwable = reportedexception.getCause();
+
+@@ -495,7 +504,17 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a, this.world)); // Paper - Anti-Xray
@@ -1343,36 +2097,30 @@ index a439277813..6ca98b7ad5 100644
+ // Paper start - Async chunk io
+ };
+ CompletableFuture> ret = new CompletableFuture<>();
-+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(this.world, chunkcoordintpair.x, chunkcoordintpair.z,
-+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, (chunkData) -> {
-+ PlayerChunkMap.this.executor.execute(() -> {
-+ Throwable throwable = null;
-+
-+ if (chunkData.chunkData == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE ||
-+ chunkData.poiData == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) {
-+ throwable = new Throwable("See log from the file io thread above");
-+ }
-+
-+ ret.complete(callable.apply(throwable != null ? null : chunkData, throwable));
++ this.world.asyncLoadManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z,
++ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, (ChunkRegionLoader.InProgressChunkHolder holder) -> {
++ PlayerChunkMap.this.executor.addTask(() -> {
++ ret.complete(callable.apply(holder, null));
+ });
-+ }, true, true, false);
++ }, false);
+ return ret;
+ // Paper end
}
private CompletableFuture> b(PlayerChunk playerchunk, ChunkStatus chunkstatus) {
-@@ -701,18 +728,40 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+@@ -701,18 +720,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
return this.v.get();
}
+ // Paper start - async chunk io
+ private boolean writeDataAsync(ChunkCoordIntPair chunkPos, NBTTagCompound poiData, NBTTagCompound chunkData, boolean async) {
+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z,
-+ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
++ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
+
+ if (async) {
+ return true;
+ }
++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSaveIOWait.startTiming()) { // Paper
+ Boolean successPoi = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, true);
+ Boolean successChunk = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, false);
+
@@ -1383,6 +2131,7 @@ index a439277813..6ca98b7ad5 100644
+ // null indicates no task existed, which means our write completed before we waited on it
+
+ return true;
++ } // Paper
+ }
+ // Paper end
+
@@ -1391,8 +2140,8 @@ index a439277813..6ca98b7ad5 100644
+ // Paper start - async param
+ return this.saveChunk(ichunkaccess, false);
+ }
-+
+ public boolean saveChunk(IChunkAccess ichunkaccess, boolean async) {
++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) {
+ NBTTagCompound poiData = this.getVillagePlace().getData(ichunkaccess.getPos()); // Paper
+ //this.n.a(ichunkaccess.getPos()); // Delay
+ // Paper end
@@ -1410,12 +2159,13 @@ index a439277813..6ca98b7ad5 100644
ichunkaccess.setLastSaved(this.world.getTime());
ichunkaccess.setNeedsSaving(false);
-@@ -724,20 +773,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+@@ -723,27 +766,33 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+ NBTTagCompound nbttagcompound;
if (chunkstatus.getType() != ChunkStatus.Type.LEVELCHUNK) {
++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveOverwriteCheck.startTiming()) { // Paper
// Paper start - Optimize save by using status cache
-- ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkcoordintpair);
-+ ChunkStatus statusOnDisk = this.getChunkStatus(chunkcoordintpair, true); // Paper - Async chunk io
+ ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkcoordintpair);
if (statusOnDisk != null && statusOnDisk.getType() == ChunkStatus.Type.LEVELCHUNK) {
// Paper end
+ this.writeDataAsync(ichunkaccess.getPos(), poiData, null, async); // Paper - Async chunk io
@@ -1427,16 +2177,26 @@ index a439277813..6ca98b7ad5 100644
return false;
}
}
-
+-
++ } // Paper
++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveDataSerialization.startTiming()) { // Paper
nbttagcompound = ChunkRegionLoader.saveChunk(this.world, ichunkaccess);
- this.write(chunkcoordintpair, nbttagcompound);
- return true;
++ } // Paper
+ return this.writeDataAsync(ichunkaccess.getPos(), poiData, nbttagcompound, async); // Paper - Async chunk io
+ //return true; // Paper
} catch (Exception exception) {
PlayerChunkMap.LOGGER.error("Failed to save chunk {},{}", chunkcoordintpair.x, chunkcoordintpair.z, exception);
com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper
-@@ -808,6 +859,62 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+ return false;
+ }
+ }
++ } // Paper
+ }
+
+ protected void setViewDistance(int i) {
+@@ -808,6 +857,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
return Iterables.unmodifiableIterable(this.visibleChunks.values());
}
@@ -1446,8 +2206,8 @@ index a439277813..6ca98b7ad5 100644
+ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException {
+ if (Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) {
+ NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ false, true, true).join().chunkData;
++ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
++ false, true, true).join().chunkData;
+
+ if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) {
+ throw new IOException("See logs for further detail");
@@ -1461,11 +2221,11 @@ index a439277813..6ca98b7ad5 100644
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException {
+ if (Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) {
+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
++ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+
+ Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world,
-+ chunkcoordintpair.x, chunkcoordintpair.z, true, false);
++ chunkcoordintpair.x, chunkcoordintpair.z, true, false);
+
+ if (ret == Boolean.FALSE) {
+ throw new IOException("See logs for further detail");
@@ -1474,43 +2234,95 @@ index a439277813..6ca98b7ad5 100644
+ }
+ super.write(chunkcoordintpair, nbttagcompound);
+ }
-+
-+ public ChunkStatus getChunkStatus(ChunkCoordIntPair chunkPos, boolean load) throws IOException {
-+ synchronized (this) {
-+ // we enter a synchronized block here so that we do not potentially use a closed regionfile
-+ RegionFile regionFile = this.getRegionFile(chunkPos, false);
-+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
-+
-+ if (!load || status != null || !regionFile.chunkExists(chunkPos)) {
-+ return status;
-+ }
-+
-+ // at this stage we need to load chunk data, however it's best we do that outside of the synchronized block
-+ }
-+
-+ NBTTagCompound compound = this.readChunkData(chunkPos);
-+
-+ // In order to avoid a race condition where a regionfile is re-loaded concurrently we directly use the status in
-+ // the returned compound. readChunkData will update the regionfile
-+ return compound == null ? null : ChunkRegionLoader.getStatus(compound);
-+ }
+ // Paper end
+
@Nullable
public NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { // Paper - private -> public
NBTTagCompound nbttagcompound = this.read(chunkcoordintpair);
-@@ -822,7 +929,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
- return null;
- }
+@@ -830,12 +915,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
-- this.updateChunkStatusOnDisk(chunkcoordintpair, nbttagcompound);
-+ synchronized (this) { // Async chunk io - Synchronize so we do not potentially get and use a closed region file
-+ this.getRegionFile(chunkcoordintpair, false).setStatus(chunkcoordintpair.x, chunkcoordintpair.z, ChunkRegionLoader.getStatus(nbttagcompound));
+ // Paper start - chunk status cache "api"
+ public ChunkStatus getChunkStatusOnDiskIfCached(ChunkCoordIntPair chunkPos) {
++ // Paper start - async io
++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false);
++
++ if (inProgressWrite != null) {
++ return ChunkRegionLoader.getStatus(inProgressWrite);
++ }
++ // Paper end
++
+ RegionFile regionFile = this.getRegionFileIfLoaded(chunkPos);
+
+ return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+ }
+
+ public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException {
++ // Paper start - async io
++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false);
++
++ if (inProgressWrite != null) {
++ return ChunkRegionLoader.getStatus(inProgressWrite);
++ }
++ // Paper end
++ synchronized (this) { // Paper - async io
+ RegionFile regionFile = this.getRegionFile(chunkPos, false);
+
+ if (!regionFile.chunkExists(chunkPos)) {
+@@ -847,17 +950,49 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+ if (status != null) {
+ return status;
+ }
++ // Paper start - async io
+ }
- return nbttagcompound;
- // Paper end
-@@ -1197,6 +1306,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+- this.readChunkData(chunkPos);
++ NBTTagCompound compound = this.readChunkData(chunkPos);
+
+- return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
++ return ChunkRegionLoader.getStatus(compound);
++ // Paper end
+ }
+
+ public void updateChunkStatusOnDisk(ChunkCoordIntPair chunkPos, @Nullable NBTTagCompound compound) throws IOException {
++ synchronized (this) { // Paper - async io
+ RegionFile regionFile = this.getRegionFile(chunkPos, false);
+
+ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound));
++ } // Paper - async io
+ }
++
++ // Paper start - async io
++ // this function will not load chunk data off disk to check for status
++ // ret null for unknown, empty for empty status on disk or absent from disk
++ public ChunkStatus getStatusOnDiskNoLoad(int x, int z) {
++ // Paper start - async io
++ net.minecraft.server.NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
++ .getPendingWrite(this.world, x, z, false);
++
++ if (inProgressWrite != null) {
++ return net.minecraft.server.ChunkRegionLoader.getStatus(inProgressWrite);
++ }
++ // Paper end
++ // variant of PlayerChunkMap#getChunkStatusOnDisk that does not load data off disk, but loads the region file
++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z);
++ synchronized (world.getChunkProvider().playerChunkMap) {
++ net.minecraft.server.RegionFile file;
++ try {
++ file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false);
++ } catch (IOException ex) {
++ throw new RuntimeException(ex);
++ }
++
++ return !file.chunkExists(chunkPos) ? ChunkStatus.EMPTY : file.getStatusIfCached(x, z);
++ }
++ }
++ // Paper end
+ // Paper end
+
+ // Spigot Start
+@@ -1197,6 +1332,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
@@ -1519,21 +2331,10 @@ index a439277813..6ca98b7ad5 100644
return this.n;
}
diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java
-index 3e80f6c53e..2f10152404 100644
+index 66c8b0307f..2ee4b88f09 100644
--- a/src/main/java/net/minecraft/server/RegionFile.java
+++ b/src/main/java/net/minecraft/server/RegionFile.java
-@@ -53,6 +53,10 @@ public class RegionFile implements AutoCloseable {
- final int location = this.getChunkLocation(new ChunkCoordIntPair(x, z));
- return this.statuses[location];
- }
-+
-+ public ChunkStatus getStatus(int x, int z, PlayerChunkMap playerChunkMap) throws IOException {
-+ return playerChunkMap.getChunkStatus(new ChunkCoordIntPair(x, z), true);
-+ }
- // Paper end
-
- public RegionFile(File file) throws IOException {
-@@ -337,7 +341,7 @@ public class RegionFile implements AutoCloseable {
+@@ -337,7 +337,7 @@ public class RegionFile implements AutoCloseable {
this.writeInt(i); // Paper - Avoid 3 io write calls
}
@@ -1572,7 +2373,7 @@ index e0fdf5f90f..d9283b36b6 100644
RegionFile regionfile = a(pos, true);
diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java
-index a343a7b31d..570b01a6cb 100644
+index a343a7b31d..7584174eb7 100644
--- a/src/main/java/net/minecraft/server/RegionFileSection.java
+++ b/src/main/java/net/minecraft/server/RegionFileSection.java
@@ -24,7 +24,7 @@ public class RegionFileSection extends RegionFi
@@ -1603,11 +2404,11 @@ index a343a7b31d..570b01a6cb 100644
+ return optional.get(); // Paper - decompile fix
} else {
- R r0 = (MinecraftSerializable) this.f.apply(() -> {
-+ R r0 = this.f.apply(() -> { // Paper - decompile fix
++ R r0 = this.f.apply(() -> { // Paper - decompile fix
this.a(i);
});
-@@ -94,7 +94,13 @@ public class RegionFileSection extends RegionFi
+@@ -94,7 +94,12 @@ public class RegionFileSection extends RegionFi
}
private void b(ChunkCoordIntPair chunkcoordintpair) {
@@ -1615,14 +2416,13 @@ index a343a7b31d..570b01a6cb 100644
+ // Paper start - load data in function
+ this.loadInData(chunkcoordintpair, this.c(chunkcoordintpair));
+ }
-+
+ public void loadInData(ChunkCoordIntPair chunkPos, NBTTagCompound compound) {
+ this.a(chunkPos, DynamicOpsNBT.a, compound);
+ // Paper end
}
@Nullable
-@@ -123,7 +129,7 @@ public class RegionFileSection extends RegionFi
+@@ -123,7 +128,7 @@ public class RegionFileSection extends RegionFi
for (int l = 0; l < 16; ++l) {
long i1 = SectionPosition.a(chunkcoordintpair, l).v();
Optional optional = optionaldynamic.get(Integer.toString(l)).get().map((dynamic2) -> {
@@ -1631,7 +2431,7 @@ index a343a7b31d..570b01a6cb 100644
this.a(i1);
}, dynamic2);
});
-@@ -142,7 +148,7 @@ public class RegionFileSection extends RegionFi
+@@ -142,7 +147,7 @@ public class RegionFileSection extends RegionFi
}
private void d(ChunkCoordIntPair chunkcoordintpair) {
@@ -1640,7 +2440,7 @@ index a343a7b31d..570b01a6cb 100644
NBTBase nbtbase = (NBTBase) dynamic.getValue();
if (nbtbase instanceof NBTTagCompound) {
-@@ -157,6 +163,20 @@ public class RegionFileSection extends RegionFi
+@@ -157,6 +162,20 @@ public class RegionFileSection extends RegionFi
}
@@ -1661,7 +2461,7 @@ index a343a7b31d..570b01a6cb 100644
private Dynamic a(ChunkCoordIntPair chunkcoordintpair, DynamicOps dynamicops) {
Map map = Maps.newHashMap();
-@@ -193,9 +213,9 @@ public class RegionFileSection extends RegionFi
+@@ -193,9 +212,9 @@ public class RegionFileSection extends RegionFi
public void a(ChunkCoordIntPair chunkcoordintpair) {
if (!this.d.isEmpty()) {
for (int i = 0; i < 16; ++i) {
@@ -1673,7 +2473,7 @@ index a343a7b31d..570b01a6cb 100644
this.d(chunkcoordintpair);
return;
}
-@@ -203,4 +223,21 @@ public class RegionFileSection extends RegionFi
+@@ -203,4 +222,21 @@ public class RegionFileSection extends RegionFi
}
}
@@ -1695,11 +2495,23 @@ index a343a7b31d..570b01a6cb 100644
+ }
+ // Paper end
}
+diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
+index 5acb0732c3..0ed2d2fbf9 100644
+--- a/src/main/java/net/minecraft/server/TicketType.java
++++ b/src/main/java/net/minecraft/server/TicketType.java
+@@ -22,6 +22,7 @@ public class TicketType {
+ public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit
+ public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit
+ public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray
++ public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
+
+ public static TicketType a(String s, Comparator comparator) {
+ return new TicketType<>(s, comparator, 0L);
diff --git a/src/main/java/net/minecraft/server/VillagePlace.java b/src/main/java/net/minecraft/server/VillagePlace.java
-index 7bc473e1ef..9f4b1b4c49 100644
+index 7bc473e1ef..8e82326c47 100644
--- a/src/main/java/net/minecraft/server/VillagePlace.java
+++ b/src/main/java/net/minecraft/server/VillagePlace.java
-@@ -20,8 +20,17 @@ public class VillagePlace extends RegionFileSection {
+@@ -20,8 +20,16 @@ public class VillagePlace extends RegionFileSection {
private final VillagePlace.a a = new VillagePlace.a();
@@ -1709,7 +2521,6 @@ index 7bc473e1ef..9f4b1b4c49 100644
+ // Paper start
+ this(file, datafixer, null);
+ }
-+
+ public VillagePlace(File file, DataFixer datafixer, WorldServer world) {
+ // Paper end
super(file, VillagePlaceSection::new, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK);
@@ -1717,7 +2528,7 @@ index 7bc473e1ef..9f4b1b4c49 100644
}
public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) {
-@@ -128,7 +137,20 @@ public class VillagePlace extends RegionFileSection {
+@@ -128,7 +136,23 @@ public class VillagePlace extends RegionFileSection {
@Override
public void a(BooleanSupplier booleansupplier) {
@@ -1730,25 +2541,28 @@ index 7bc473e1ef..9f4b1b4c49 100644
+ while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) {
+ ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).u();
+
-+ NBTTagCompound data = this.getData(chunkcoordintpair);
++ NBTTagCompound data;
++ try (co.aikar.timings.Timing ignored1 = this.world.timings.poiSaveDataSerialization.startTiming()) {
++ data = this.getData(chunkcoordintpair);
++ }
+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
-+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
++ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
+ }
+ }
+ // Paper end
this.a.a();
}
-@@ -164,7 +186,7 @@ public class VillagePlace extends RegionFileSection {
+@@ -164,7 +188,7 @@ public class VillagePlace extends RegionFileSection {
}
private static boolean a(ChunkSection chunksection) {
- Stream stream = VillagePlaceType.e();
-+ Stream stream = VillagePlaceType.e(); // Paper - decompile fix
++ Stream stream = VillagePlaceType.e(); // Paper - decompile fix
chunksection.getClass();
return stream.anyMatch(chunksection::a);
-@@ -214,6 +236,42 @@ public class VillagePlace extends RegionFileSection {
+@@ -214,6 +238,42 @@ public class VillagePlace extends RegionFileSection {
}
}
@@ -1758,8 +2572,8 @@ index 7bc473e1ef..9f4b1b4c49 100644
+ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws java.io.IOException {
+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) {
+ NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ true, false, true).join().poiData;
++ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
++ true, false, true).join().poiData;
+
+ if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) {
+ throw new java.io.IOException("See logs for further detail");
@@ -1773,11 +2587,11 @@ index 7bc473e1ef..9f4b1b4c49 100644
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws java.io.IOException {
+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) {
+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
++ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+
+ Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world,
-+ chunkcoordintpair.x, chunkcoordintpair.z, true, true);
++ chunkcoordintpair.x, chunkcoordintpair.z, true, true);
+
+ if (ret == Boolean.FALSE) {
+ throw new java.io.IOException("See logs for further detail");
@@ -1791,7 +2605,7 @@ index 7bc473e1ef..9f4b1b4c49 100644
public static enum Occupancy {
HAS_SPACE(VillagePlaceRecord::d), IS_OCCUPIED(VillagePlaceRecord::e), ANY((villageplacerecord) -> {
-@@ -222,7 +280,7 @@ public class VillagePlace extends RegionFileSection {
+@@ -222,7 +282,7 @@ public class VillagePlace extends RegionFileSection {
private final Predicate super VillagePlaceRecord> d;
@@ -1801,10 +2615,10 @@ index 7bc473e1ef..9f4b1b4c49 100644
}
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index 47005dcfdc..f0380c5df4 100644
+index 47005dcfdc..f7597d499f 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -75,6 +75,78 @@ public class WorldServer extends World {
+@@ -75,6 +75,79 @@ public class WorldServer extends World {
return new Throwable(entity + " Added to world at " + new java.util.Date());
}
@@ -1878,11 +2692,83 @@ index 47005dcfdc..f0380c5df4 100644
+ }
+ }
+ };
++ public final com.destroystokyo.paper.io.chunk.ChunkLoadTaskManager asyncLoadManager;
+ // Paper end
+
// Add env and gen to constructor
public WorldServer(MinecraftServer minecraftserver, Executor executor, WorldNBTStorage worldnbtstorage, WorldData worlddata, DimensionManager dimensionmanager, GameProfilerFiller gameprofilerfiller, WorldLoadListener worldloadlistener, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
super(worlddata, dimensionmanager, (world, worldprovider) -> {
+@@ -119,6 +192,7 @@ public class WorldServer extends World {
+
+ this.mobSpawnerTrader = this.worldProvider.getDimensionManager().getType() == DimensionManager.OVERWORLD ? new MobSpawnerTrader(this) : null; // CraftBukkit - getType()
+ this.getServer().addWorld(this.getWorld()); // CraftBukkit
++ this.asyncLoadManager = new com.destroystokyo.paper.io.chunk.ChunkLoadTaskManager(this, 4); // todo CONFIGURABLE // Paper
+ }
+
+ public void doTick(BooleanSupplier booleansupplier) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index c5321c5076..d3d43f3b77 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -531,22 +531,23 @@ public class CraftWorld implements World {
+ }
+
+ if (!generate) {
+- net.minecraft.server.RegionFile file;
+- try {
+- file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false);
+- } catch (IOException ex) {
+- throw new RuntimeException(ex);
+- }
++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); // Paper - async io - move to own method
+
+- ChunkStatus status = file.getStatusIfCached(x, z);
+- if (!file.chunkExists(chunkPos) || (status != null && status != ChunkStatus.FULL)) {
++ // Paper start - async io
++ if (status == ChunkStatus.EMPTY) {
++ // does not exist on disk
+ return false;
+ }
+
++ if (status == null) { // at this stage we don't know what it is on disk
+ IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.EMPTY, true);
+ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) {
+ return false;
+ }
++ } else if (status != ChunkStatus.FULL) {
++ return false; // not full status on disk
++ }
++ // Paper end
+
+ // fall through to load
+ // we do this so we do not re-read the chunk data on disk
+@@ -2323,16 +2324,17 @@ public class CraftWorld implements World {
+
+ @Override
+ public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) {
+- // TODO placeholder
+- if (Bukkit.isPrimaryThread()) {
+- return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen));
+- } else {
+- CompletableFuture ret = new CompletableFuture<>();
+- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
+- ret.complete(getChunkAtGen(x, z, gen));
+- });
+- return ret;
++ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z);
++ if (immediate != null) {
++ return CompletableFuture.completedFuture(immediate.bukkitChunk);
+ }
++
++ CompletableFuture ret = new CompletableFuture<>();
++ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> {
++ ret.complete(chunk == null ? null : chunk.bukkitChunk);
++ });
++
++ return ret;
+ }
+ // Paper end
+
--
2.20.1
diff --git a/patches/server/0052-Asynchronous-chunk-loading-api.patch b/patches/server/0052-Asynchronous-chunk-loading-api.patch
deleted file mode 100644
index c8e4b1d05..000000000
--- a/patches/server/0052-Asynchronous-chunk-loading-api.patch
+++ /dev/null
@@ -1,244 +0,0 @@
-From d7a653379c6a6446a42907cd03e9d1348844f893 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Mon, 8 Jul 2019 03:24:59 -0700
-Subject: [PATCH] Asynchronous chunk loading api
-
----
- .../minecraft/server/ChunkProviderServer.java | 134 ++++++++++++++++++
- .../net/minecraft/server/ChunkStatus.java | 1 +
- .../java/net/minecraft/server/MCUtil.java | 5 +
- .../java/net/minecraft/server/RegionFile.java | 2 +-
- .../java/net/minecraft/server/TicketType.java | 1 +
- .../org/bukkit/craftbukkit/CraftWorld.java | 19 +--
- 6 files changed, 152 insertions(+), 10 deletions(-)
-
-diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-index db9113994e..b46285ecdc 100644
---- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
-+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-@@ -147,6 +147,140 @@ public class ChunkProviderServer extends IChunkProvider {
- return playerChunk.getAvailableChunkNow();
-
- }
-+
-+ private long asyncLoadSeqCounter;
-+
-+ public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) {
-+ if (Thread.currentThread() != this.serverThread) {
-+ this.serverThreadQueue.execute(() -> {
-+ this.getChunkAtAsynchronously(x, z, gen, onComplete);
-+ });
-+ return;
-+ }
-+
-+ long k = ChunkCoordIntPair.pair(x, z);
-+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z);
-+
-+ IChunkAccess ichunkaccess;
-+
-+ // try cache
-+ for (int l = 0; l < 4; ++l) {
-+ if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) {
-+ ichunkaccess = this.cacheChunk[l];
-+ if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
-+
-+ // move to first in cache
-+
-+ for (int i1 = 3; i1 > 0; --i1) {
-+ this.cachePos[i1] = this.cachePos[i1 - 1];
-+ this.cacheStatus[i1] = this.cacheStatus[i1 - 1];
-+ this.cacheChunk[i1] = this.cacheChunk[i1 - 1];
-+ }
-+
-+ this.cachePos[0] = k;
-+ this.cacheStatus[0] = ChunkStatus.FULL;
-+ this.cacheChunk[0] = ichunkaccess;
-+
-+ onComplete.accept((Chunk)ichunkaccess);
-+
-+ return;
-+ }
-+ }
-+ }
-+
-+ if (gen) {
-+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
-+ return;
-+ }
-+
-+ IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions
-+ if (current != null) {
-+ if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) {
-+ onComplete.accept(null); // the chunk is not gen'd
-+ return;
-+ }
-+ // we know the chunk is at full status here (either in read-only mode or the real thing)
-+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
-+ return;
-+ } else {
-+ RegionFile file;
-+
-+ try {
-+ file = this.world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false);
-+ } catch (IOException ex) {
-+ throw new RuntimeException(ex);
-+ }
-+
-+ ChunkStatus status;
-+ if (!file.chunkExists(chunkPos) || ((status = file.getStatusIfCached(x, z)) != null && status != ChunkStatus.FULL)) {
-+ onComplete.accept(null); // cached status says not generated, or data does not exist on disk
-+ return;
-+ }
-+
-+
-+ if (status == ChunkStatus.FULL) {
-+ // at this stage we know it is fully generated, but is on disk
-+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete);
-+ return;
-+ }
-+
-+ // at this stage we don't know what status the chunk is in
-+ }
-+
-+ // here we don't know what status it is and we're not supposed to generate
-+ // so we asynchronously load empty status
-+
-+ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> {
-+ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) {
-+ // the chunk on disk was not a full status chunk
-+ onComplete.accept(null);
-+ return;
-+ }
-+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required
-+ });
-+ }
-+
-+ private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) {
-+ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete);
-+ }
-+
-+
-+ private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) {
-+ CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true);
-+ long identifier = this.asyncLoadSeqCounter++;
-+ int ticketLevel = MCUtil.getTicketLevelFor(status);
-+ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
-+
-+ future.whenCompleteAsync((Either either, Throwable throwable) -> {
-+ // either left -> success
-+ // either right -> failure
-+
-+ if (throwable != null) {
-+ throw new RuntimeException(throwable);
-+ }
-+
-+ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
-+ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading
-+
-+ Optional failure = either.right();
-+
-+ if (failure.isPresent()) {
-+ // failure
-+ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString());
-+ }
-+
-+ onComplete.accept(either.left().get());
-+
-+ }, this.serverThreadQueue);
-+ }
-+
-+ public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
-+ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
-+ }
-+
-+ public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
-+ this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
-+ }
- // Paper end
-
- @Nullable
-diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java
-index e324989b46..abb0d69d2f 100644
---- a/src/main/java/net/minecraft/server/ChunkStatus.java
-+++ b/src/main/java/net/minecraft/server/ChunkStatus.java
-@@ -153,6 +153,7 @@ public class ChunkStatus {
- return ChunkStatus.q.size();
- }
-
-+ public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER
- public static int a(ChunkStatus chunkstatus) {
- return ChunkStatus.r.getInt(chunkstatus.c());
- }
-diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
-index 23d1935dd5..14f8b61042 100644
---- a/src/main/java/net/minecraft/server/MCUtil.java
-+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -530,4 +530,9 @@ public final class MCUtil {
- out.print(fileData);
- }
- }
-+
-+ public static int getTicketLevelFor(ChunkStatus status) {
-+ // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean)
-+ return 33 + ChunkStatus.getTicketLevelOffset(status);
-+ }
- }
-diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java
-index 66c8b0307f..3e80f6c53e 100644
---- a/src/main/java/net/minecraft/server/RegionFile.java
-+++ b/src/main/java/net/minecraft/server/RegionFile.java
-@@ -310,7 +310,7 @@ public class RegionFile implements AutoCloseable {
- return this.c[this.f(chunkcoordintpair)];
- }
-
-- public final boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER
-+ public boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER
- public boolean d(ChunkCoordIntPair chunkcoordintpair) {
- return this.getOffset(chunkcoordintpair) != 0;
- }
-diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
-index 5acb0732c3..0ed2d2fbf9 100644
---- a/src/main/java/net/minecraft/server/TicketType.java
-+++ b/src/main/java/net/minecraft/server/TicketType.java
-@@ -22,6 +22,7 @@ public class TicketType {
- public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit
- public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit
- public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray
-+ public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
-
- public static TicketType a(String s, Comparator comparator) {
- return new TicketType<>(s, comparator, 0L);
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index c5321c5076..7691f23316 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -2323,16 +2323,17 @@ public class CraftWorld implements World {
-
- @Override
- public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) {
-- // TODO placeholder
-- if (Bukkit.isPrimaryThread()) {
-- return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen));
-- } else {
-- CompletableFuture ret = new CompletableFuture<>();
-- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
-- ret.complete(getChunkAtGen(x, z, gen));
-- });
-- return ret;
-+ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z);
-+ if (immediate != null) {
-+ return CompletableFuture.completedFuture(immediate.bukkitChunk);
- }
-+
-+ CompletableFuture ret = new CompletableFuture<>();
-+ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> {
-+ ret.complete(chunk == null ? null : chunk.bukkitChunk);
-+ });
-+
-+ return ret;
- }
- // Paper end
-
---
-2.20.1
-
diff --git a/patches/server/0054-Implement-optional-per-player-mob-spawns.patch b/patches/server/0053-Implement-optional-per-player-mob-spawns.patch
similarity index 96%
rename from patches/server/0054-Implement-optional-per-player-mob-spawns.patch
rename to patches/server/0053-Implement-optional-per-player-mob-spawns.patch
index 1893abeb1..1e74bd275 100644
--- a/patches/server/0054-Implement-optional-per-player-mob-spawns.patch
+++ b/patches/server/0053-Implement-optional-per-player-mob-spawns.patch
@@ -1,4 +1,4 @@
-From 854429ab6cd52dfd88f48b40206055b8790085f0 Mon Sep 17 00:00:00 2001
+From 3ff085046cf1d3efc53b2dec884c4d5b75cd0df3 Mon Sep 17 00:00:00 2001
From: kickash32
Date: Tue, 11 Jun 2019 22:22:16 -0400
Subject: [PATCH] Implement optional per player mob spawns
@@ -29,10 +29,10 @@ index ff520d9e86..5ed02f6485 100644
public boolean asynchronous;
public EngineMode engineMode;
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-index f6a6421140..770ee018fe 100644
+index 4f7c442264..9f6c362dd1 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-@@ -586,9 +586,21 @@ public class ChunkProviderServer extends IChunkProvider {
+@@ -582,9 +582,21 @@ public class ChunkProviderServer extends IChunkProvider {
// Paper start - only allow spawns upto the limit per chunk and update count afterwards
int currEntityCount = object2intmap.getInt(enumcreaturetype);
int difference = k1 - currEntityCount;
@@ -57,7 +57,7 @@ index f6a6421140..770ee018fe 100644
}
}
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
-index 6ca98b7ad5..56e60e0ce1 100644
+index f25ca782b9..a235df4185 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -105,6 +105,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
@@ -68,7 +68,7 @@ index 6ca98b7ad5..56e60e0ce1 100644
private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
double d0 = (double) (chunkcoordintpair.x * 16 + 8);
double d1 = (double) (chunkcoordintpair.z * 16 + 8);
-@@ -1131,6 +1132,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+@@ -1157,6 +1158,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
}
@@ -167,7 +167,7 @@ index af397dd1f7..5e001733a9 100644
@Nullable
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index f0380c5df4..0c4fd5ca4d 100644
+index f7597d499f..2410db3353 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
@@ -17,6 +17,9 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -188,7 +188,7 @@ index f0380c5df4..0c4fd5ca4d 100644
// CraftBukkit start
private int tickPosition;
-@@ -930,6 +934,7 @@ public class WorldServer extends World {
+@@ -932,6 +936,7 @@ public class WorldServer extends World {
}
public Object2IntMap l() {
@@ -196,7 +196,7 @@ index f0380c5df4..0c4fd5ca4d 100644
Object2IntMap object2intmap = new Object2IntOpenHashMap();
ObjectIterator objectiterator = this.entitiesById.values().iterator();
-@@ -957,13 +962,47 @@ public class WorldServer extends World {
+@@ -959,13 +964,47 @@ public class WorldServer extends World {
}
// Paper end
object2intmap.mergeInt(enumcreaturetype, 1, Integer::sum);
diff --git a/patches/server/0055-Tulips-change-fox-type.patch b/patches/server/0054-Tulips-change-fox-type.patch
similarity index 98%
rename from patches/server/0055-Tulips-change-fox-type.patch
rename to patches/server/0054-Tulips-change-fox-type.patch
index 270d74073..3a3895508 100644
--- a/patches/server/0055-Tulips-change-fox-type.patch
+++ b/patches/server/0054-Tulips-change-fox-type.patch
@@ -1,4 +1,4 @@
-From 4c6e788cb3b1158e8955dccd8434939ebe1a026a Mon Sep 17 00:00:00 2001
+From dc255f32785831cfa041105a377ea880926762f8 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sat, 13 Jul 2019 15:56:22 -0500
Subject: [PATCH] Tulips change fox type