diff --git a/patches/server/Actually-unload-POI-data.patch b/patches/server/Actually-unload-POI-data.patch
deleted file mode 100644
index d94a20ed52..0000000000
--- a/patches/server/Actually-unload-POI-data.patch
+++ /dev/null
@@ -1,321 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Mon, 31 Aug 2020 11:08:17 -0700
-Subject: [PATCH] Actually unload POI data
-
-While it's not likely for a poi data leak to be meaningful,
-sometimes it is.
-
-This patch also prevents the saving/unloading of POI data when
-world saving is disabled.
-
-diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/ChunkSystem.java
-+++ b/src/main/java/net/minecraft/server/ChunkSystem.java
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
- for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
- chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
- }
-+ chunkMap.getPoiManager().dequeueUnload(holder.pos.longKey); // Paper - unload POI data
- }
-
- public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
- for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
- chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
- }
-+ chunkMap.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
- }
-
- public static void onChunkBorder(LevelChunk chunk, ChunkHolder holder) {
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- private void processUnloads(BooleanSupplier shouldKeepTicking) {
- LongIterator longiterator = this.toDrop.iterator();
-- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
-+ for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) { // Paper - diff on change
- long j = longiterator.nextLong();
- ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- this.poiManager.loadInData(pos, chunkHolder.poiData);
- chunkHolder.tasks.forEach(Runnable::run);
-+ this.getPoiManager().dequeueUnload(pos.longKey); // Paper
-
- if (chunkHolder.protoChunk != null) {
- ProtoChunk protochunk = chunkHolder.protoChunk;
-diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -0,0 +0,0 @@
- package net.minecraft.world.entity.ai.village.poi;
-
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
- import com.mojang.datafixers.DataFixer;
- import com.mojang.datafixers.util.Pair;
- import it.unimi.dsi.fastutil.longs.Long2ByteMap;
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.storage.SectionStorage;
- public class PoiManager extends SectionStorage {
- public static final int MAX_VILLAGE_DISTANCE = 6;
- public static final int VILLAGE_SECTION_SIZE = 1;
-- private final PoiManager.DistanceTracker distanceTracker;
-+ // Paper start - unload poi data
-+ // the vanilla tracker needs to be replaced because it does not support level removes
-+ private final io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D();
-+ static final int POI_DATA_SOURCE = 7;
-+ public static int convertBetweenLevels(final int level) {
-+ return POI_DATA_SOURCE - level;
-+ }
-+
-+ protected void updateDistanceTracking(long section) {
-+ if (this.isVillageCenter(section)) {
-+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
-+ } else {
-+ this.villageDistanceTracker.removeSource(section);
-+ }
-+ }
-+ // Paper end - unload poi data
- private final LongSet loadedChunks = new LongOpenHashSet();
- public final net.minecraft.server.level.ServerLevel world; // Paper // Paper public
-
- public PoiManager(Path path, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world) {
- super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, registryManager, world);
-+ if (world == null) { throw new IllegalStateException("world must be non-null"); } // Paper - require non-null
- this.world = (net.minecraft.server.level.ServerLevel)world; // Paper
-- this.distanceTracker = new PoiManager.DistanceTracker();
- }
-
-+ // Paper start - actually unload POI data
-+ private final java.util.TreeSet queuedUnloads = new java.util.TreeSet<>();
-+ private final Long2ObjectOpenHashMap queuedUnloadsByCoordinate = new Long2ObjectOpenHashMap<>();
-+
-+ static final class QueuedUnload implements Comparable {
-+
-+ private final long unloadTick;
-+ private final long coordinate;
-+
-+ public QueuedUnload(long unloadTick, long coordinate) {
-+ this.unloadTick = unloadTick;
-+ this.coordinate = coordinate;
-+ }
-+
-+ @Override
-+ public int compareTo(QueuedUnload other) {
-+ if (other.unloadTick == this.unloadTick) {
-+ return Long.compare(this.coordinate, other.coordinate);
-+ } else {
-+ return Long.compare(this.unloadTick, other.unloadTick);
-+ }
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ int hash = 1;
-+ hash = hash * 31 + Long.hashCode(this.unloadTick);
-+ hash = hash * 31 + Long.hashCode(this.coordinate);
-+ return hash;
-+ }
-+
-+ @Override
-+ public boolean equals(Object obj) {
-+ if (obj == null || obj.getClass() != QueuedUnload.class) {
-+ return false;
-+ }
-+ QueuedUnload other = (QueuedUnload)obj;
-+ return other.unloadTick == this.unloadTick && other.coordinate == this.coordinate;
-+ }
-+ }
-+
-+ long determineDelay(long coordinate) {
-+ if (this.isEmpty(coordinate)) {
-+ return 5 * 60 * 20;
-+ } else {
-+ return 60 * 20;
-+ }
-+ }
-+
-+ public void queueUnload(long coordinate, long minTarget) {
-+ io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload queue");
-+ QueuedUnload unload = new QueuedUnload(minTarget + this.determineDelay(coordinate), coordinate);
-+ QueuedUnload existing = this.queuedUnloadsByCoordinate.put(coordinate, unload);
-+ if (existing != null) {
-+ this.queuedUnloads.remove(existing);
-+ }
-+ this.queuedUnloads.add(unload);
-+ }
-+
-+ public void dequeueUnload(long coordinate) {
-+ io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload dequeue");
-+ QueuedUnload unload = this.queuedUnloadsByCoordinate.remove(coordinate);
-+ if (unload != null) {
-+ this.queuedUnloads.remove(unload);
-+ }
-+ }
-+
-+ public void pollUnloads(BooleanSupplier canSleepForTick) {
-+ io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload");
-+ long currentTick = net.minecraft.server.MinecraftServer.currentTickLong;
-+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.world.getChunkSource();
-+ net.minecraft.server.level.ChunkMap playerChunkMap = chunkProvider.chunkMap;
-+ // copied target determination from PlayerChunkMap
-+
-+ java.util.Iterator iterator = this.queuedUnloads.iterator();
-+ for (int i = 0; iterator.hasNext() && (i < 200 || this.queuedUnloads.size() > 2000 || canSleepForTick.getAsBoolean()); i++) {
-+ QueuedUnload unload = iterator.next();
-+ if (unload.unloadTick > currentTick) {
-+ break;
-+ }
-+
-+ long coordinate = unload.coordinate;
-+
-+ iterator.remove();
-+ this.queuedUnloadsByCoordinate.remove(coordinate);
-+
-+ if (playerChunkMap.getUnloadingChunkHolder(net.minecraft.server.MCUtil.getCoordinateX(coordinate), net.minecraft.server.MCUtil.getCoordinateZ(coordinate)) != null
-+ || playerChunkMap.getUpdatingChunkIfPresent(coordinate) != null) {
-+ continue;
-+ }
-+
-+ this.unloadData(coordinate);
-+ }
-+ }
-+
-+ @Override
-+ public void unloadData(long coordinate) {
-+ io.papermc.paper.util.TickThread.softEnsureTickThread("async unloading poi data");
-+ super.unloadData(coordinate);
-+ }
-+
-+ @Override
-+ protected void onUnload(long coordinate) {
-+ io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload callback");
-+ this.loadedChunks.remove(coordinate);
-+ int chunkX = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
-+ int chunkZ = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
-+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-+ long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
-+ this.updateDistanceTracking(sectionPos);
-+ }
-+ }
-+ // Paper end - actually unload POI data
-+
- public void add(BlockPos pos, Holder type) {
- this.getOrCreate(SectionPos.asLong(pos)).add(pos, type);
- }
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
- }
-
- public int sectionsToVillage(SectionPos pos) {
-- this.distanceTracker.runAllUpdates();
-- return this.distanceTracker.getLevel(pos.asLong());
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util
-+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util
- }
-
- boolean isVillageCenter(long pos) {
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
- @Override
- public void tick(BooleanSupplier shouldKeepTicking) {
- // Paper start - async chunk io
-- while (!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean()) {
-+ while (!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean() && !this.world.noSave()) { // Paper - unload POI data - don't write to disk if saving is disabled
- ChunkPos chunkcoordintpair = SectionPos.of(this.dirty.firstLong()).chunk();
-
- net.minecraft.nbt.CompoundTag data;
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
- com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
- chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
- }
-+ // Paper start - unload POI data
-+ if (!this.world.noSave()) { // don't write to disk if saving is disabled
-+ this.pollUnloads(shouldKeepTicking);
-+ }
-+ // Paper end - unload POI data
- // Paper end
-- this.distanceTracker.runAllUpdates();
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking until
- }
-
- @Override
- protected void setDirty(long pos) {
- super.setDirty(pos);
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ this.updateDistanceTracking(pos); // Paper - move to new distance tracking util
- }
-
- @Override
- protected void onSectionLoad(long pos) {
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ this.updateDistanceTracking(pos); // Paper - move to new distance tracking util
- }
-
- public void checkConsistencyWithBlocks(ChunkPos chunkPos, LevelChunkSection chunkSection) {
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
-
- @Override
- protected int getLevelFromSource(long id) {
-- return PoiManager.this.isVillageCenter(id) ? 0 : 7;
-+ return PoiManager.this.isVillageCenter(id) ? 0 : 7; // Paper - unload poi data - diff on change, this specifies the source level to use for distance tracking
- }
-
- @Override
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-@@ -0,0 +0,0 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl
- // Paper - remove mojang I/O thread
- }
-
-+ // Paper start - actually unload POI data
-+ public void unloadData(long coordinate) {
-+ ChunkPos chunkPos = new ChunkPos(coordinate);
-+ this.flush(chunkPos);
-+
-+ Long2ObjectMap> data = this.storage;
-+ int before = data.size();
-+
-+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-+ data.remove(SectionPos.asLong(chunkPos.x, section, chunkPos.z));
-+ }
-+
-+ if (before != data.size()) {
-+ this.onUnload(coordinate);
-+ }
-+ }
-+
-+ protected void onUnload(long coordinate) {}
-+
-+ public boolean isEmpty(long coordinate) {
-+ Long2ObjectMap> data = this.storage;
-+ int x = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
-+ int z = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
-+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-+ Optional optional = data.get(SectionPos.asLong(x, section, z));
-+ if (optional != null && optional.orElse(null) != null) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+ // Paper end - actually unload POI data
-+
- protected void tick(BooleanSupplier shouldKeepTicking) {
- while(this.hasWork() && shouldKeepTicking.getAsBoolean()) {
- ChunkPos chunkPos = SectionPos.of(this.dirty.firstLong()).chunk();
-@@ -0,0 +0,0 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl
- });
- }
- }
-+ if (this instanceof net.minecraft.world.entity.ai.village.poi.PoiManager) { ((net.minecraft.world.entity.ai.village.poi.PoiManager)this).queueUnload(pos.longKey, net.minecraft.server.MinecraftServer.currentTickLong + 1); } // Paper - unload POI data
-
- }
-
diff --git a/patches/server/Add-API-for-quit-reason.patch b/patches/server/Add-API-for-quit-reason.patch
index 24044b5270..6e14736474 100644
--- a/patches/server/Add-API-for-quit-reason.patch
+++ b/patches/server/Add-API-for-quit-reason.patch
@@ -29,7 +29,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
-
+ public boolean isRealPlayer; // Paper
public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks
public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper
+ public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - there are a lot of changes to do if we change all methods leading to the event
diff --git a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
index acf1da0faa..4cc97b3cde 100644
--- a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
+++ b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
@@ -34,7 +34,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@ public abstract class PlayerList {
public void placeNewPlayer(Connection connection, ServerPlayer player) {
- player.isRealPlayer = true; // Paper - Chunk priority
+ player.isRealPlayer = true; // Paper
+ player.loginTime = System.currentTimeMillis(); // Paper
GameProfile gameprofile = player.getGameProfile();
GameProfileCache usercache = this.server.getProfileCache();
diff --git a/patches/server/Add-Alternate-Current-redstone-implementation.patch b/patches/server/Add-Alternate-Current-redstone-implementation.patch
index 0c593e04d8..626b58a963 100644
--- a/patches/server/Add-Alternate-Current-redstone-implementation.patch
+++ b/patches/server/Add-Alternate-Current-redstone-implementation.patch
@@ -2020,7 +2020,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
return new Throwable(entity + " Added to world at " + new java.util.Date());
}
@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
- return this.entityManager.canPositionTick(pos.toLong()); // Paper
+ // Paper end - rewrite chunk system
}
+ // Paper start - optimize redstone (Alternate Current)
diff --git a/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch b/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
index 31a33111ac..f9533d8250 100644
--- a/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
+++ b/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
@@ -69,7 +69,7 @@ diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
private static WatchdogThread instance;
private long timeoutTime;
private boolean restart;
@@ -80,7 +80,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
private volatile long lastTick;
private volatile boolean stopping;
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
super( "Paper Watchdog Thread" );
this.timeoutTime = timeoutTime;
this.restart = restart;
@@ -89,7 +89,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
private static long monotonicMillis()
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
while ( !this.stopping )
{
//
@@ -110,7 +110,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "The server has stopped responding! This is (probably) not a Paper bug." ); // Paper
log.log( Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author" );
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
}
}
// Paper end
@@ -122,7 +122,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // Paper end - Different message for short timeout
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
- com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//
diff --git a/patches/server/Add-debug-for-sync-chunk-loads.patch b/patches/server/Add-debug-for-sync-chunk-loads.patch
index 4c89009e21..9909c2cb52 100644
--- a/patches/server/Add-debug-for-sync-chunk-loads.patch
+++ b/patches/server/Add-debug-for-sync-chunk-loads.patch
@@ -207,8 +207,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
import java.util.ArrayList;
@@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
commands.put(Set.of("version"), new VersionCommand());
- commands.put(Set.of("debug", "chunkinfo"), new ChunkDebugCommand());
commands.put(Set.of("fixlight"), new FixLightCommand());
+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
+ commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
return commands.entrySet().stream()
@@ -302,21 +302,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
- com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1);
+ // Paper start - async chunk io/loading
+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system
// Paper end
+ com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x1, z1); // Paper - sync load info
this.level.timings.syncChunkLoad.startTiming(); // Paper
chunkproviderserver_b.managedBlock(completablefuture::isDone);
- com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - async chunk debug // Paper - rewrite chunk system
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
- };
- public final com.destroystokyo.paper.io.chunk.ChunkTaskManager asyncChunkTaskManager;
- // Paper end
+ return this.entityLookup;
+ }
+ // Paper end - rewrite chunk system
+ // Paper start
+ @Override
+ public boolean hasChunk(int chunkX, int chunkZ) {
diff --git a/patches/server/Add-exception-reporting-event.patch b/patches/server/Add-exception-reporting-event.patch
index 9e713bf0fc..32296cc141 100644
--- a/patches/server/Add-exception-reporting-event.patch
+++ b/patches/server/Add-exception-reporting-event.patch
@@ -48,18 +48,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return internalTask;
+ }
+}
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- return true;
- } catch (Exception exception) {
- ChunkMap.LOGGER.error("Failed to save chunk {},{}", new Object[]{chunkcoordintpair.x, chunkcoordintpair.z, exception});
-+ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper
- return false;
- }
- }
diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
diff --git a/patches/server/Add-more-async-catchers.patch b/patches/server/Add-more-async-catchers.patch
deleted file mode 100644
index c589d9650d..0000000000
--- a/patches/server/Add-more-async-catchers.patch
+++ /dev/null
@@ -1,44 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Thu, 15 Jul 2021 01:41:53 -0700
-Subject: [PATCH] Add more async catchers
-
-
-diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-@@ -0,0 +0,0 @@ public class EntityTickList {
- }
-
- public void add(Entity entity) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper
- this.ensureActiveIsNotIterated();
- this.active.put(entity.getId(), entity);
- }
-
- public void remove(Entity entity) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper
- this.ensureActiveIsNotIterated();
- this.active.remove(entity.getId());
- }
-@@ -0,0 +0,0 @@ public class EntityTickList {
- }
-
- public void forEach(Consumer action) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper
- if (this.iterated != null) {
- throw new UnsupportedOperationException("Only one concurrent iteration supported");
- } else {
-diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A
- }
-
- public void updateChunkStatus(ChunkPos chunkPos, ChunkHolder.FullChunkStatus levelType) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous chunk ticking status update"); // Paper
- Visibility visibility = Visibility.fromFullChunkStatus(levelType);
-
- this.updateChunkStatus(chunkPos, visibility);
diff --git a/patches/server/Add-packet-limiter-config.patch b/patches/server/Add-packet-limiter-config.patch
index b78aa8d090..86e1736c20 100644
--- a/patches/server/Add-packet-limiter-config.patch
+++ b/patches/server/Add-packet-limiter-config.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Fri, 30 Oct 2020 22:37:16 -0700
Subject: [PATCH] Add packet limiter config
diff --git a/patches/server/Add-paper-mobcaps-and-paper-playermobcaps.patch b/patches/server/Add-paper-mobcaps-and-paper-playermobcaps.patch
index 6926c53646..46d4be37c1 100644
--- a/patches/server/Add-paper-mobcaps-and-paper-playermobcaps.patch
+++ b/patches/server/Add-paper-mobcaps-and-paper-playermobcaps.patch
@@ -22,7 +22,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
import io.papermc.paper.command.subcommands.SyncLoadInfoCommand;
import io.papermc.paper.command.subcommands.VersionCommand;
@@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
- commands.put(Set.of("fixlight"), new FixLightCommand());
+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
commands.put(Set.of("dumpitem"), new DumpItemCommand());
+ commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
diff --git a/patches/server/Add-velocity-warnings.patch b/patches/server/Add-velocity-warnings.patch
index 5225494b80..814b5c62d6 100644
--- a/patches/server/Add-velocity-warnings.patch
+++ b/patches/server/Add-velocity-warnings.patch
@@ -65,7 +65,7 @@ diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
log.log( Level.SEVERE, "During the run of the server, a physics stackoverflow was supressed" );
log.log( Level.SEVERE, "near " + net.minecraft.world.level.Level.lastPhysicsProblem );
}
@@ -85,4 +85,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // Paper end
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
- com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system
diff --git a/patches/server/Allow-controlled-flushing-for-network-manager.patch b/patches/server/Allow-controlled-flushing-for-network-manager.patch
index 1a685bd7ae..07cd0d19ef 100644
--- a/patches/server/Allow-controlled-flushing-for-network-manager.patch
+++ b/patches/server/Allow-controlled-flushing-for-network-manager.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Sat, 4 Apr 2020 15:27:44 -0700
Subject: [PATCH] Allow controlled flushing for network manager
@@ -110,8 +110,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
if (callbacks != null) {
channelfuture.addListener((future) -> {
@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
- }
private boolean processQueue() {
+ try { // Paper - add pending task queue
if (this.queue.isEmpty()) return true;
+ // Paper start - make only one flush call per sendPacketQueue() call
+ final boolean needsFlush = this.canFlush;
diff --git a/patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch b/patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch
deleted file mode 100644
index 61ade74f6b..0000000000
--- a/patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch
+++ /dev/null
@@ -1,89 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sat, 19 Jun 2021 22:47:17 -0700
-Subject: [PATCH] Allow removal/addition of entities to entity ticklist during
- tick
-
-It really doesn't make any sense that we would iterate over removed
-entities during tick. Sure - tick entity checks removed, but
-does it check if the entity is in an entity ticking chunk?
-No it doesn't. So, allowing removal while iteration
-ENSURES only entities MARKED TO TICK are ticked.
-
-diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-@@ -0,0 +0,0 @@ import javax.annotation.Nullable;
- import net.minecraft.world.entity.Entity;
-
- public class EntityTickList {
-- private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>();
-- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>();
-- @Nullable
-- private Int2ObjectMap iterated;
-+ private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking?
-
- private void ensureActiveIsNotIterated() {
-- if (this.iterated == this.active) {
-- this.passive.clear();
--
-- for(Int2ObjectMap.Entry entry : Int2ObjectMaps.fastIterable(this.active)) {
-- this.passive.put(entry.getIntKey(), entry.getValue());
-- }
--
-- Int2ObjectMap int2ObjectMap = this.active;
-- this.active = this.passive;
-- this.passive = int2ObjectMap;
-- }
-+ // Paper - replace with better logic, do not delay removals
-
- }
-
- public void add(Entity entity) {
- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper
- this.ensureActiveIsNotIterated();
-- this.active.put(entity.getId(), entity);
-+ this.entities.add(entity); // Paper - replace with better logic, do not delay removals/additions
- }
-
- public void remove(Entity entity) {
- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper
- this.ensureActiveIsNotIterated();
-- this.active.remove(entity.getId());
-+ this.entities.remove(entity); // Paper - replace with better logic, do not delay removals/additions
- }
-
- public boolean contains(Entity entity) {
-- return this.active.containsKey(entity.getId());
-+ return this.entities.contains(entity); // Paper - replace with better logic, do not delay removals/additions
- }
-
- public void forEach(Consumer action) {
- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper
-- if (this.iterated != null) {
-- throw new UnsupportedOperationException("Only one concurrent iteration supported");
-- } else {
-- this.iterated = this.active;
--
-- try {
-- for(Entity entity : this.active.values()) {
-- action.accept(entity);
-- }
-- } finally {
-- this.iterated = null;
-+ // Paper start - replace with better logic, do not delay removals/additions
-+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
-+ // (by dfl iterator() is configured to not iterate over new entries)
-+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator();
-+ try {
-+ while (iterator.hasNext()) {
-+ action.accept(iterator.next());
- }
--
-+ } finally {
-+ iterator.finishedIterating();
- }
-+ // Paper end - replace with better logic, do not delay removals/additions
- }
- }
diff --git a/patches/server/Anti-Xray.patch b/patches/server/Anti-Xray.patch
index c83e59b616..035a79f979 100644
--- a/patches/server/Anti-Xray.patch
+++ b/patches/server/Anti-Xray.patch
@@ -1047,30 +1047,12 @@ diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/j
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- completablefuture1.thenAcceptAsync((either) -> {
- either.ifLeft((chunk) -> {
- this.tickingGenerated.getAndIncrement();
-- MutableObject mutableobject = new MutableObject();
-+ MutableObject> mutableobject = new MutableObject<>(); // Paper - Anti-Xray - Bypass
-
- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> {
- this.playerLoadedChunk(entityplayer, mutableobject, chunk);
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- while (objectiterator.hasNext()) {
- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
- ChunkPos chunkcoordintpair = playerchunk.getPos();
-- MutableObject mutableobject = new MutableObject();
-+ MutableObject> mutableobject = new MutableObject<>(); // Paper - Anti-Xray - Bypass
-
- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> {
- SectionPos sectionposition = entityplayer.getLastSectionPos();
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
-- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) {
-+ protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - Anti-Xray - Bypass
+- public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public
++ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass
if (player.level == this.level) {
if (newWithinViewDistance && !oldWithinViewDistance) {
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
@@ -1205,9 +1187,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) {
- super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), inhabitedTime, sectionArrayInitializer, blendingData);
+ super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry
- this.tickersInLevel = Maps.newHashMap();
- this.clientLightReady = false;
- this.level = (ServerLevel) world; // CraftBukkit - type
+ // Paper start - rewrite light engine
+ this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
+ this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -1560,7 +1542,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
- if (playersInRange.isEmpty()) return;
+ if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader
- ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null, true);
+ // Paper start - Anti-Xray - Bypass
@@ -1575,8 +1557,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }));
+ // Paper end
}
- });
- });
+ // Paper - rewrite player chunk loader
+
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
diff --git a/patches/server/Async-GameProfileCache-saving.patch b/patches/server/Async-GameProfileCache-saving.patch
index 5a99f753b6..2d278a5625 100644
--- a/patches/server/Async-GameProfileCache-saving.patch
+++ b/patches/server/Async-GameProfileCache-saving.patch
@@ -16,7 +16,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ this.getProfileCache().save(false); // Paper
}
// Spigot end
- com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.close(true, true); // Paper
+ io.papermc.paper.chunk.system.io.RegionFileIOThread.close(true); // Paper // Paper - rewrite chunk system
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
diff --git a/patches/server/Async-catch-modifications-to-critical-entity-state.patch b/patches/server/Async-catch-modifications-to-critical-entity-state.patch
index ce05bbdfd2..2285755002 100644
--- a/patches/server/Async-catch-modifications-to-critical-entity-state.patch
+++ b/patches/server/Async-catch-modifications-to-critical-entity-state.patch
@@ -17,8 +17,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
private boolean addEntityUuid(T entity) {
+ org.spigotmc.AsyncCatcher.catchOp("Entity add by UUID"); // Paper
if (!this.knownUuids.add(entity.getUUID())) {
- // Paper start
- T conflict = this.visibleEntityStorage.getEntity(entity.getUUID());
+ PersistentEntitySectionManager.LOGGER.warn("UUID of added entity already exists: {}", entity);
+ return false;
@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A
}
@@ -116,13 +116,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
while (!longset.isEmpty()) {
@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A
- long i = SectionPos.asLong(blockposition); final long newSectionPos = i; // Paper - diff on change, new position section
+ long i = SectionPos.asLong(blockposition);
if (i != this.currentSectionKey) {
+ org.spigotmc.AsyncCatcher.catchOp("Entity move"); // Paper
- PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Paper
- Visibility visibility = this.currentSection.getStatus(); final Visibility oldVisibility = visibility; // Paper - diff on change - this should be OLD section visibility
- // Paper start
+ Visibility visibility = this.currentSection.getStatus();
+
+ if (!this.currentSection.remove(this.entity)) {
@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A
@Override
diff --git a/patches/server/Asynchronous-chunk-IO-and-loading.patch b/patches/server/Asynchronous-chunk-IO-and-loading.patch
deleted file mode 100644
index 685709fd5e..0000000000
--- a/patches/server/Asynchronous-chunk-IO-and-loading.patch
+++ /dev/null
@@ -1,3516 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sat, 13 Jul 2019 09:23:10 -0700
-Subject: [PATCH] Asynchronous chunk IO and loading
-
-ChunkSerializer needs the new tick lists to be saved (see added todos)
-
-This patch re-adds a file IO thread as well as shoving de-serializing
-chunk NBT data onto worker threads. This patch also will shove
-chunk data serialization onto the same worker threads when the chunk
-is unloaded - this cannot be done for regular saves since that's unsafe.
-
-The file IO Thread
-
-Unlike 1.13 and below, the file IO thread is prioritized - IO tasks can
-be reoredered, however they are "stuck" to a world & coordinate.
-
-Scheduling IO tasks works as follows, given a world & coordinate - location:
-
-The IO thread has been designed to ensure that reads and writes appear to
-occur synchronously for a given location, however the implementation also
-has the unfortunate side-effect of making every write appear as if
-they occur without failure.
-
-The IO thread has also been designed to accomodate Mojang's decision to
-store chunk data and POI data separately. It can independently schedule
-tasks for each.
-
-However threads can wait for writes to complete and check if:
- - The write was overwriten by another scheduler
- - The write failed (however it does not indicate whether it was overwritten by another scheduler)
-
-Scheduling reads:
-
- - If a write task is in progress, the task is not scheduled and returns the in-progress write data
- This means that readers cannot modify the NBTTagCompound returned and must clone if it they wish to write
- - If a write task is not in progress but a read task is in progress, then the read task is simply chained
- This means that again, readers cannot modify the NBTTagCompound returned
-
-Scheduling writes:
-
- - If a read task is in progress, ignore the read task and schedule the write
- We cannot complete the read task since we assume it wants old data - not current
- - If a write task is pending, overwrite the write data
- The file IO thread does correctly handle cases where the data is overwritten when it
- is writing data (before completing a task it will check if the data was overwritten and
- will retry).
-
-When the file IO thread executes a task for a location, the it will
-execute the read task first (if it exists), then it will execute the
-write task. This ensures that, even when scheduling at different
-priorities, that reads/writes for a location act synchronously.
-
-The downside of the file IO thread is that write failure can only be
-indicated to the scheduling thread if:
-
-- No other thread decides to schedule another write for the location
-concurrently
-- The scheduling thread blocks on the write to complete (however the
-current implementation can be modified to indicate success
-asynchronously)
-
-The file io thread can be modified easily to provide indications
-of write failure and write overwriting if needed.
-
-The upside of the file IO thread is that if a write failures, then
-chunk data is not lost until server restart. This leaves more room
-for spurious failure.
-
-Finally, the io thread will indicate to the console when reads
-or writes fail - with relevant detail.
-
-Asynchronous chunk data serialization for unloading chunks
-
-When chunks unload they make a call to PlayerChunkMap#saveChunk(IChunkAccess).
-Even if I make the IO asynchronous for this call, the data serialization
-still hits pretty hard. And given that now the chunk system will
-aggressively unload chunks more often (queued immediately at
-ticket level 45 or higher), unloads occur more often, and
-combined with our changes to the unload queue to make it
-significantly more aggresive - chunk unloads can hit pretty hard.
-Especially players running around with elytras and fireworks.
-
-For serializing chunk data off main, there are some tasks which cannot be
-done asynchronously. Lighting data must be saved beforehand as well as
-potentially some tick lists. These are completed before scheduling the
-asynchronous save.
-
-However serializing chunk data off of the main thread is still risky.
-Even though this patch schedules the save to occur after ALL references
-of the chunk are removed from the world, plugins can still technically
-access entities inside the chunks. For this, if the serialization task
-fails for any reason, it will be re-scheduled to be serialized on the
-main thread - with the hopes that the reason it failed was due to a plugin
-and not an error with the save code itself. Like vanilla code - if the
-serialization fails, the chunk data is lost.
-
-Asynchronous chunk io/loading
-
-Mojang's current implementation for loading chunk data off disk is
-to return a CompletableFuture that will be completed by scheduling a
-task to be executed on the world's chunk queue (which is only drained
-on the main thread). This task will read the IO off disk and it will
-apply data conversions & deserialization synchronously. Obviously
-all 3 of these operations are expensive however all can be completed
-asynchronously instead.
-
-The solution this patch uses is as follows:
-
-0. If an asynchronous chunk save is in progress (see above), wait
-for that task to complete. It will use the serialized NBTTagCompound
-created by the task. If the task fails to complete, then we would continue
-with step 1. If it does not, we skip step 1. (Note: We actually load
-POI data no matter what in this case).
-1. Schedule an IO task to read chunk & poi data off disk.
-2. The IO task will schedule a chunk load task.
-3. The chunk load task executes on the async chunk loader threads
-and will apply datafixers & de-serialize the chunk into a ProtoChunk
-or ProtoChunkExtension.
-4. The in progress chunk is then passed on to the world's chunk queue
-to complete the ComletableFuture and execute any of the synchronous
-tasks required to be executed by the chunk load task (i.e lighting
-and some poi tasks).
-
-diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-@@ -0,0 +0,0 @@ public class WorldTimingsHandler {
-
- public final Timing miscMobSpawning;
-
-+ public final Timing poiUnload;
-+ public final Timing chunkUnload;
-+ public final Timing poiSaveDataSerialization;
-+ public final Timing chunkSave;
-+ public final Timing chunkSaveDataSerialization;
-+ public final Timing chunkSaveIOWait;
-+ public final Timing chunkUnloadPrepareSave;
-+ public final Timing chunkUnloadPOISerialization;
-+ public final Timing chunkUnloadDataSave;
-+
- public WorldTimingsHandler(Level server) {
- String name = ((PrimaryLevelData) server.getLevelData()).getLevelName() + " - ";
-
-@@ -0,0 +0,0 @@ public class WorldTimingsHandler {
-
-
- miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc");
-+
-+ 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");
-+ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization");
-+ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait");
-+ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare");
-+ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization");
-+ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization");
- }
-
- public static Timing getTickList(ServerLevel worldserver, String timingsType) {
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io;
-+
-+import org.bukkit.Bukkit;
-+
-+public final class IOUtil {
-+
-+ /* Copied from concrete or concurrentutil */
-+
-+ public static long getCoordinateKey(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
-+
-+ public static int getCoordinateX(final long key) {
-+ return (int)key;
-+ }
-+
-+ public static int getCoordinateZ(final long key) {
-+ return (int)(key >>> 32);
-+ }
-+
-+ public static int getRegionCoordinate(final int chunkCoordinate) {
-+ return chunkCoordinate >> 5;
-+ }
-+
-+ public static int getChunkInRegion(final int chunkCoordinate) {
-+ return chunkCoordinate & 31;
-+ }
-+
-+ public static String genericToString(final Object object) {
-+ return object == null ? "null" : object.getClass().getName() + ":" + object.toString();
-+ }
-+
-+ public static T notNull(final T obj) {
-+ if (obj == null) {
-+ throw new NullPointerException();
-+ }
-+ return obj;
-+ }
-+
-+ public static T notNull(final T obj, final String msgIfNull) {
-+ if (obj == null) {
-+ throw new NullPointerException(msgIfNull);
-+ }
-+ return obj;
-+ }
-+
-+ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
-+ if (off < 0 || len < 0 || (arrayLength - off) < len) {
-+ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
-+ }
-+ }
-+
-+ public static int getPriorityForCurrentThread() {
-+ return Bukkit.isPrimaryThread() ? PrioritizedTaskQueue.HIGHEST_PRIORITY : PrioritizedTaskQueue.NORMAL_PRIORITY;
-+ }
-+
-+ @SuppressWarnings("unchecked")
-+ public static void rethrow(final Throwable throwable) throws T {
-+ throw (T)throwable;
-+ }
-+
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io;
-+
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.storage.RegionFile;
-+import org.slf4j.Logger;
-+
-+import java.io.IOException;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.Consumer;
-+import java.util.function.Function;
-+
-+/**
-+ * Prioritized singleton thread responsible for all chunk IO that occurs in a minecraft server.
-+ *
-+ *
-+ * 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.
-+ *
-+ *
-+ *
-+ * @see Holder#INSTANCE
-+ * @see #scheduleSave(ServerLevel, int, int, CompoundTag, CompoundTag, int)
-+ * @see #loadChunkDataAsync(ServerLevel, int, int, int, Consumer, boolean, boolean, boolean)
-+ */
-+public final class PaperFileIOThread extends QueueExecutorThread {
-+
-+ public static final Logger LOGGER = LogUtils.getLogger();
-+ public static final CompoundTag FAILURE_VALUE = new CompoundTag();
-+
-+ public static final class Holder {
-+
-+ public static final PaperFileIOThread INSTANCE = new PaperFileIOThread();
-+
-+ static {
-+ INSTANCE.start();
-+ }
-+ }
-+
-+ private final AtomicLong writeCounter = new AtomicLong();
-+
-+ private PaperFileIOThread() {
-+ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time
-+ this.setName("Paper 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) -> {
-+ LOGGER.error("Uncaught exception thrown from IO thread, report this!", thr);
-+ });
-+ }
-+
-+ /* run() is implemented by superclass */
-+
-+ /*
-+ *
-+ * IO thread will perform reads before writes
-+ *
-+ * How reads/writes are scheduled:
-+ *
-+ * If read in progress while scheduling write, ignore read and schedule write
-+ * If read in progress while scheduling read (no write in progress), chain the read task
-+ *
-+ *
-+ * If write in progress while scheduling read, use the pending write data and ret immediately
-+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
-+ *
-+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
-+ * it fails to properly propagate write failures. When writes fail the data is kept so future reads will actually
-+ * read the failed write data. This should hopefully act as a way to prevent data loss for spurious fails for writing data.
-+ *
-+ */
-+
-+ /**
-+ * 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 priority Priority level to try to bump to
-+ */
-+ public void bumpPriority(final ServerLevel world, final int chunkX, final int chunkZ, final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority: " + priority);
-+ }
-+
-+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
-+
-+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
-+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
-+
-+ if (poiTask != null) {
-+ poiTask.raisePriority(priority);
-+ }
-+ if (chunkTask != null) {
-+ chunkTask.raisePriority(priority);
-+ }
-+ }
-+
-+ public CompoundTag getPendingWrite(final ServerLevel 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 priority Priority level to set to
-+ */
-+ public void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority: " + priority);
-+ }
-+
-+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
-+
-+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
-+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
-+
-+ if (poiTask != null) {
-+ poiTask.updatePriority(priority);
-+ }
-+ if (chunkTask != null) {
-+ chunkTask.updatePriority(priority);
-+ }
-+ }
-+
-+ /**
-+ * Schedules the chunk data to be written asynchronously.
-+ *
-+ * 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.
-+ *
-+ *
-+ * 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 chunkData Chunk data. If {@code null}, then no chunk data is saved.
-+ * @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.
-+ */
-+ public void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final CompoundTag poiData, final CompoundTag chunkData,
-+ final int priority) throws IllegalArgumentException {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority: " + priority);
-+ }
-+
-+ final long writeCounter = this.writeCounter.getAndIncrement();
-+
-+ if (poiData != null) {
-+ this.scheduleWrite(world.poiDataController, world, chunkX, chunkZ, poiData, priority, writeCounter);
-+ }
-+ if (chunkData != null) {
-+ this.scheduleWrite(world.chunkDataController, world, chunkX, chunkZ, chunkData, priority, writeCounter);
-+ }
-+ }
-+
-+ private void scheduleWrite(final ChunkDataController dataController, final ServerLevel world,
-+ final int chunkX, final int chunkZ, final CompoundTag data, final int priority, final long writeCounter) {
-+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask taskRunning) -> {
-+ if (taskRunning == null) {
-+ // no task is scheduled
-+
-+ // create task
-+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
-+ newTask.inProgressWrite = new ChunkDataController.InProgressWrite();
-+ newTask.inProgressWrite.writeCounter = writeCounter;
-+ newTask.inProgressWrite.data = data;
-+
-+ PaperFileIOThread.this.queueTask(newTask); // schedule
-+ return newTask;
-+ }
-+
-+ taskRunning.raisePriority(priority);
-+
-+ if (taskRunning.inProgressWrite == null) {
-+ taskRunning.inProgressWrite = new ChunkDataController.InProgressWrite();
-+ }
-+
-+ boolean reschedule = taskRunning.inProgressWrite.writeCounter == -1L;
-+
-+ // synchronize for readers
-+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
-+ synchronized (taskRunning) {
-+ taskRunning.inProgressWrite.data = data;
-+ taskRunning.inProgressWrite.writeCounter = writeCounter;
-+ }
-+
-+ if (reschedule) {
-+ // We need to reschedule this task since the previous one is not currently scheduled since it failed
-+ taskRunning.reschedule(priority);
-+ }
-+
-+ return taskRunning;
-+ });
-+ }
-+
-+ /**
-+ * Same as {@link #loadChunkDataAsync(ServerLevel, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns
-+ * 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}.
-+ *
-+ */
-+ public CompletableFuture loadChunkDataAsyncFuture(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final int priority, final boolean readPoiData, final boolean readChunkData,
-+ final boolean intendingToBlock) {
-+ final CompletableFuture future = new CompletableFuture<>();
-+ this.loadChunkDataAsync(world, chunkX, chunkZ, priority, future::complete, readPoiData, readChunkData, intendingToBlock);
-+ return future;
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously.
-+ *
-+ * Impl notes:
-+ *
-+ *
-+ * 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.
-+ *
-+ *
-+ * 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 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.
-+ */
-+ public void loadChunkDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final int priority, final Consumer onComplete,
-+ final boolean readPoiData, final boolean readChunkData,
-+ final boolean intendingToBlock) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority: " + priority);
-+ }
-+
-+ if (!(readPoiData | readChunkData)) {
-+ throw new IllegalArgumentException("Must read chunk data or poi data");
-+ }
-+
-+ final ChunkData complete = new ChunkData();
-+ final boolean[] requireCompletion = new boolean[] { readPoiData, readChunkData };
-+
-+ if (readPoiData) {
-+ this.scheduleRead(world.poiDataController, world, chunkX, chunkZ, (final CompoundTag poiData) -> {
-+ complete.poiData = poiData;
-+
-+ final boolean finished;
-+
-+ // avoid a race condition where the file io thread completes and we complete synchronously
-+ // Note: Synchronization can be elided if both of the accesses are volatile
-+ synchronized (requireCompletion) {
-+ requireCompletion[0] = false; // 0 -> poi data
-+ finished = !requireCompletion[1]; // 1 -> chunk data
-+ }
-+
-+ if (finished) {
-+ onComplete.accept(complete);
-+ }
-+ }, priority, intendingToBlock);
-+ }
-+
-+ if (readChunkData) {
-+ this.scheduleRead(world.chunkDataController, world, chunkX, chunkZ, (final CompoundTag chunkData) -> {
-+ complete.chunkData = chunkData;
-+
-+ final boolean finished;
-+
-+ // avoid a race condition where the file io thread completes and we complete synchronously
-+ // Note: Synchronization can be elided if both of the accesses are volatile
-+ synchronized (requireCompletion) {
-+ requireCompletion[1] = false; // 1 -> chunk data
-+ finished = !requireCompletion[0]; // 0 -> poi data
-+ }
-+
-+ if (finished) {
-+ onComplete.accept(complete);
-+ }
-+ }, priority, intendingToBlock);
-+ }
-+
-+ }
-+
-+ // Note: the onComplete may be called asynchronously or synchronously here.
-+ private void scheduleRead(final ChunkDataController dataController, final ServerLevel world,
-+ final int chunkX, final int chunkZ, final Consumer onComplete, final int priority,
-+ final boolean intendingToBlock) {
-+
-+ Function tryLoadFunction = (final RegionFile file) -> {
-+ if (file == null) {
-+ return Boolean.TRUE;
-+ }
-+ return Boolean.valueOf(file.hasChunk(new ChunkPos(chunkX, chunkZ)));
-+ };
-+
-+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask running) -> {
-+ if (running == null) {
-+ // not scheduled
-+
-+ final Boolean shouldSchedule = intendingToBlock ? dataController.computeForRegionFile(chunkX, chunkZ, tryLoadFunction) :
-+ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction);
-+
-+ if (shouldSchedule == Boolean.FALSE) {
-+ // not on disk
-+ onComplete.accept(null);
-+ return null;
-+ }
-+
-+ // set up task
-+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
-+ newTask.inProgressRead = new ChunkDataController.InProgressRead();
-+ newTask.inProgressRead.readFuture.thenAccept(onComplete);
-+
-+ PaperFileIOThread.this.queueTask(newTask); // schedule task
-+ return newTask;
-+ }
-+
-+ running.raisePriority(priority);
-+
-+ if (running.inProgressWrite == null) {
-+ // chain to the read future
-+ running.inProgressRead.readFuture.thenAccept(onComplete);
-+ return running;
-+ }
-+
-+ // at this stage we have to use the in progress write's data to avoid an order issue
-+ // we don't synchronize since all writes to data occur in the compute() call
-+ onComplete.accept(running.inProgressWrite.data);
-+ return running;
-+ });
-+ }
-+
-+ /**
-+ * Same as {@link #loadChunkDataAsync(ServerLevel, 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 ServerLevel world, final int chunkX, final int chunkZ, final int priority,
-+ final boolean readPoiData, final boolean readChunkData) {
-+ return this.loadChunkDataAsyncFuture(world, chunkX, chunkZ, priority, readPoiData, readChunkData, true).join();
-+ }
-+
-+ /**
-+ * Schedules the given task at the specified priority to be executed on the IO thread.
-+ *
-+ * Internal api. Do not use.
-+ *
-+ */
-+ public void runTask(final int priority, final Runnable runnable) {
-+ this.queueTask(new GeneralTask(priority, runnable));
-+ }
-+
-+ static final class GeneralTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
-+
-+ private final Runnable run;
-+
-+ public GeneralTask(final int priority, final Runnable run) {
-+ super(priority);
-+ this.run = IOUtil.notNull(run, "Task may not be null");
-+ }
-+
-+ @Override
-+ public void run() {
-+ try {
-+ this.run.run();
-+ } catch (final Throwable throwable) {
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ LOGGER.error("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable);
-+ }
-+ }
-+ }
-+
-+ public static final class ChunkData {
-+
-+ public CompoundTag poiData;
-+ public CompoundTag chunkData;
-+
-+ public ChunkData() {}
-+
-+ public ChunkData(final CompoundTag poiData, final CompoundTag chunkData) {
-+ this.poiData = poiData;
-+ this.chunkData = chunkData;
-+ }
-+ }
-+
-+ public static abstract class ChunkDataController {
-+
-+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
-+ public final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f);
-+
-+ public abstract void writeData(final int x, final int z, final CompoundTag compound) throws IOException;
-+ public abstract CompoundTag 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 {
-+ public long writeCounter;
-+ public CompoundTag data;
-+ }
-+
-+ public static final class InProgressRead {
-+ public final CompletableFuture readFuture = new CompletableFuture<>();
-+ }
-+ }
-+
-+ public static final class ChunkDataTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
-+
-+ public ChunkDataController.InProgressWrite inProgressWrite;
-+ public ChunkDataController.InProgressRead inProgressRead;
-+
-+ private final ServerLevel world;
-+ private final int x;
-+ private final int z;
-+ private final ChunkDataController taskController;
-+
-+ public ChunkDataTask(final int priority, final ServerLevel world, final int x, final int z, final ChunkDataController taskController) {
-+ super(priority);
-+ this.world = world;
-+ this.x = x;
-+ this.z = z;
-+ this.taskController = taskController;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "Task for world: '" + this.world.getWorld().getName() + "' at " + this.x + "," + this.z +
-+ " poi: " + (this.taskController == this.world.poiDataController) + ", hash: " + this.hashCode();
-+ }
-+
-+ /*
-+ *
-+ * IO thread will perform reads before writes
-+ *
-+ * How reads/writes are scheduled:
-+ *
-+ * If read in progress while scheduling write, ignore read and schedule write
-+ * If read in progress while scheduling read (no write in progress), chain the read task
-+ *
-+ *
-+ * If write in progress while scheduling read, use the pending write data and ret immediately
-+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
-+ *
-+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
-+ * it fails to properly propagate write failures
-+ *
-+ */
-+
-+ void reschedule(final int priority) {
-+ // priority is checked before this stage // TODO what
-+ this.queue.lazySet(null);
-+ this.priority.lazySet(priority);
-+ PaperFileIOThread.Holder.INSTANCE.queueTask(this);
-+ }
-+
-+ @Override
-+ public void run() {
-+ ChunkDataController.InProgressRead read = this.inProgressRead;
-+ if (read != null) {
-+ CompoundTag compound = PaperFileIOThread.FAILURE_VALUE;
-+ try {
-+ compound = this.taskController.readData(this.x, this.z);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
-+ // fall through to complete with null data
-+ }
-+ read.readFuture.complete(compound);
-+ }
-+
-+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(this.x, this.z));
-+
-+ ChunkDataController.InProgressWrite write = this.inProgressWrite;
-+
-+ if (write == null) {
-+ // IntelliJ warns this is invalid, however it does not consider that writes to the task map & the inProgress field can occur concurrently.
-+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkDataTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+ return valueInMap.inProgressWrite == null ? null : valueInMap;
-+ });
-+
-+ if (inMap == null) {
-+ return; // set the task value to null, indicating we're done
-+ }
-+
-+ // not null, which means there was a concurrent write
-+ write = this.inProgressWrite;
-+ }
-+
-+ for (;;) {
-+ final long writeCounter;
-+ final CompoundTag data;
-+
-+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
-+ synchronized (write) {
-+ writeCounter = write.writeCounter;
-+ data = write.data;
-+ }
-+
-+ boolean failedWrite = false;
-+
-+ try {
-+ this.taskController.writeData(this.x, this.z, data);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
-+ failedWrite = true;
-+ }
-+
-+ boolean finalFailWrite = failedWrite;
-+
-+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkDataTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+ if (valueInMap.inProgressWrite.writeCounter == writeCounter) {
-+ if (finalFailWrite) {
-+ valueInMap.inProgressWrite.writeCounter = -1L;
-+ }
-+
-+ return null;
-+ }
-+ return valueInMap;
-+ // Hack end
-+ });
-+
-+ if (inMap == null) {
-+ // write counter matched, so we wrote the most up-to-date pending data, we're done here
-+ // or we failed to write and successfully set the write counter to -1
-+ return; // we're done here
-+ }
-+
-+ // fetch & write new data
-+ continue;
-+ }
-+ }
-+ }
-+}
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io;
-+
-+import java.util.concurrent.ConcurrentLinkedQueue;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.concurrent.atomic.AtomicReference;
-+
-+public class PrioritizedTaskQueue {
-+
-+ // lower numbers are a higher priority (except < 0)
-+ // higher priorities are always executed before lower priorities
-+
-+ /**
-+ * Priority value indicating the task has completed or is being completed.
-+ */
-+ 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;
-+
-+ /**
-+ * 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;
-+
-+ /**
-+ * Should be used for scheduling chunk loads/generation that would increase response times to users.
-+ */
-+ public static final int HIGH_PRIORITY = 2;
-+
-+ /**
-+ * Default priority.
-+ */
-+ 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;
-+
-+ /**
-+ * Use for tasks that should "eventually" execute.
-+ */
-+ public static final int LOWEST_PRIORITY = 5;
-+
-+ private static final int TOTAL_PRIORITIES = 6;
-+
-+ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[])new ConcurrentLinkedQueue[TOTAL_PRIORITIES];
-+
-+ private final AtomicBoolean shutdown = new AtomicBoolean();
-+
-+ {
-+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
-+ this.queues[i] = new ConcurrentLinkedQueue<>();
-+ }
-+ }
-+
-+ /**
-+ * Returns whether the specified priority is valid
-+ */
-+ public static boolean validPriority(final int priority) {
-+ return priority >= 0 && priority < TOTAL_PRIORITIES;
-+ }
-+
-+ /**
-+ * 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.
-+ */
-+ public void add(final T task) throws IllegalStateException {
-+ int priority = task.getPriority();
-+ if (priority != COMPLETING_PRIORITY) {
-+ task.setQueue(this);
-+ this.queues[priority].add(task);
-+ }
-+ if (this.shutdown.get()) {
-+ // note: we're not actually sure at this point if our task will go through
-+ throw new IllegalStateException("Queue has shutdown, refusing to execute task " + IOUtil.genericToString(task));
-+ }
-+ }
-+
-+ /**
-+ * Polls the highest priority task currently available. {@code null} if none.
-+ */
-+ public T poll() {
-+ T task;
-+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
-+ final ConcurrentLinkedQueue queue = this.queues[i];
-+
-+ while ((task = queue.poll()) != null) {
-+ final int prevPriority = task.tryComplete(i);
-+ if (prevPriority != COMPLETING_PRIORITY && prevPriority <= i) {
-+ // if the prev priority was greater-than or equal to our current priority
-+ return task;
-+ }
-+ }
-+ }
-+
-+ return null;
-+ }
-+
-+ /**
-+ * Polls the highest priority task currently available. {@code null} if none.
-+ */
-+ public T poll(final int lowestPriority) {
-+ T task;
-+ final int max = Math.min(LOWEST_PRIORITY, lowestPriority);
-+ for (int i = 0; i <= max; ++i) {
-+ final ConcurrentLinkedQueue queue = this.queues[i];
-+
-+ while ((task = queue.poll()) != null) {
-+ final int prevPriority = task.tryComplete(i);
-+ if (prevPriority != COMPLETING_PRIORITY && prevPriority <= i) {
-+ // if the prev priority was greater-than or equal to our current priority
-+ return task;
-+ }
-+ }
-+ }
-+
-+ return null;
-+ }
-+
-+ /**
-+ * Returns whether this queue may have tasks queued.
-+ *
-+ * This operation is not atomic, but is MT-Safe.
-+ *
-+ * @return {@code true} if tasks may be queued, {@code false} otherwise
-+ */
-+ public boolean hasTasks() {
-+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
-+ final ConcurrentLinkedQueue queue = this.queues[i];
-+
-+ if (queue.peek() != null) {
-+ return true;
-+ }
-+ }
-+ return false;
-+ }
-+
-+ /**
-+ * 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
-+ *
-+ *
-+ * 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() {
-+ return this.shutdown.getAndSet(false);
-+ }
-+
-+ public abstract static class PrioritizedTask {
-+
-+ protected final AtomicReference queue = new AtomicReference<>();
-+
-+ protected final AtomicInteger priority;
-+
-+ protected PrioritizedTask() {
-+ this(PrioritizedTaskQueue.NORMAL_PRIORITY);
-+ }
-+
-+ protected PrioritizedTask(final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.priority = new AtomicInteger(priority);
-+ }
-+
-+ /**
-+ * Returns the current priority. Note that {@link PrioritizedTaskQueue#COMPLETING_PRIORITY} will be returned
-+ * if this task is completing or has completed.
-+ */
-+ public final int getPriority() {
-+ 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();;) {
-+ if (curr == COMPLETING_PRIORITY) {
-+ return COMPLETING_PRIORITY;
-+ }
-+ if (curr > minPriority) {
-+ // curr is lower priority
-+ return curr;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, COMPLETING_PRIORITY))) {
-+ return curr;
-+ }
-+ continue;
-+ }
-+ }
-+
-+ /**
-+ * 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() {
-+ return this.exchangePriorityVolatile(PrioritizedTaskQueue.COMPLETING_PRIORITY) != PrioritizedTaskQueue.COMPLETING_PRIORITY;
-+ }
-+
-+ /**
-+ * Attempts to raise the priority to the priority level specified.
-+ * @param priority Priority specified
-+ * @return {@code true} if successful, {@code false} otherwise.
-+ */
-+ public boolean raisePriority(final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority");
-+ }
-+
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if (curr == COMPLETING_PRIORITY) {
-+ return false;
-+ }
-+ if (priority >= curr) {
-+ return true;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
-+ PrioritizedTaskQueue queue = this.queue.get();
-+ if (queue != null) {
-+ //noinspection unchecked
-+ queue.queues[priority].add(this); // silently fail on shutdown
-+ }
-+ return true;
-+ }
-+ continue;
-+ }
-+ }
-+
-+ /**
-+ * 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.
-+ */
-+ public boolean updatePriority(final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority");
-+ }
-+
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if (curr == COMPLETING_PRIORITY) {
-+ return false;
-+ }
-+ if (curr == priority) {
-+ return true;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
-+ PrioritizedTaskQueue queue = this.queue.get();
-+ if (queue != null) {
-+ //noinspection unchecked
-+ queue.queues[priority].add(this); // silently fail on shutdown
-+ }
-+ return true;
-+ }
-+ continue;
-+ }
-+ }
-+
-+ void setQueue(final PrioritizedTaskQueue queue) {
-+ this.queue.set(queue);
-+ }
-+
-+ /* priority */
-+
-+ protected final int getPriorityVolatile() {
-+ return this.priority.get();
-+ }
-+
-+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
-+ if (this.priority.compareAndSet(expect, update)) {
-+ return expect;
-+ }
-+ return this.priority.get();
-+ }
-+
-+ protected final int exchangePriorityVolatile(final int value) {
-+ return this.priority.getAndSet(value);
-+ }
-+ }
-+}
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io;
-+
-+import com.mojang.logging.LogUtils;
-+import org.slf4j.Logger;
-+
-+import java.util.concurrent.ConcurrentLinkedQueue;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.locks.LockSupport;
-+
-+public class QueueExecutorThread extends Thread {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ protected final PrioritizedTaskQueue queue;
-+ protected final long spinWaitTime;
-+
-+ protected volatile boolean closed;
-+
-+ protected final AtomicBoolean parked = new AtomicBoolean();
-+
-+ protected volatile ConcurrentLinkedQueue flushQueue = new ConcurrentLinkedQueue<>();
-+ protected volatile long flushCycles;
-+
-+ protected int lowestPriorityToPoll = PrioritizedTaskQueue.LOWEST_PRIORITY;
-+
-+ public int getLowestPriorityToPoll() {
-+ return this.lowestPriorityToPoll;
-+ }
-+
-+ public void setLowestPriorityToPoll(final int lowestPriorityToPoll) {
-+ if (this.isAlive()) {
-+ throw new IllegalStateException("Cannot set after starting");
-+ }
-+ this.lowestPriorityToPoll = lowestPriorityToPoll;
-+ }
-+
-+ public QueueExecutorThread(final PrioritizedTaskQueue queue) {
-+ this(queue, (int)(1.e6)); // 1.0ms
-+ }
-+
-+ public QueueExecutorThread(final PrioritizedTaskQueue queue, final long spinWaitTime) { // in ms
-+ this.queue = queue;
-+ this.spinWaitTime = spinWaitTime;
-+ }
-+
-+ @Override
-+ public void run() {
-+ final long spinWaitTime = this.spinWaitTime;
-+ main_loop:
-+ for (;;) {
-+ this.pollTasks(true);
-+
-+ // spinwait
-+
-+ final long start = System.nanoTime();
-+
-+ 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
-+
-+ if (this.pollTasks(true)) {
-+ // restart loop, found tasks
-+ continue main_loop;
-+ }
-+
-+ if (this.handleClose()) {
-+ return; // we're done
-+ }
-+
-+ if ((System.nanoTime() - start) >= spinWaitTime) {
-+ break;
-+ }
-+ }
-+
-+ if (this.handleClose()) {
-+ return;
-+ }
-+
-+ this.parked.set(true);
-+
-+ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
-+ // (i.e it will not notify us)
-+ if (this.pollTasks(true)) {
-+ this.parked.set(false);
-+ continue;
-+ }
-+
-+ if (this.handleClose()) {
-+ return;
-+ }
-+
-+ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
-+ // LockSupport.park() can fail for any reason
-+ do {
-+ Thread.interrupted();
-+ LockSupport.park("Waiting on tasks");
-+ } while (this.parked.get());
-+ }
-+ }
-+
-+ protected boolean handleClose() {
-+ if (this.closed) {
-+ this.pollTasks(true); // this ensures we've emptied the queue
-+ this.handleFlushThreads(true);
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ protected boolean pollTasks(boolean flushTasks) {
-+ Runnable task;
-+ boolean ret = false;
-+
-+ while ((task = this.queue.poll(this.lowestPriorityToPoll)) != null) {
-+ ret = true;
-+ try {
-+ task.run();
-+ } catch (final Throwable throwable) {
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "': " + IOUtil.genericToString(task), throwable);
-+ }
-+ }
-+
-+ if (flushTasks) {
-+ this.handleFlushThreads(false);
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected void handleFlushThreads(final boolean shutdown) {
-+ Thread parking;
-+ ConcurrentLinkedQueue flushQueue = this.flushQueue;
-+ do {
-+ ++flushCycles; // may be plain read opaque write
-+ while ((parking = flushQueue.poll()) != null) {
-+ LockSupport.unpark(parking);
-+ }
-+ } while (this.pollTasks(false));
-+
-+ if (shutdown) {
-+ this.flushQueue = null;
-+
-+ // defend against a race condition where a flush thread double-checks right before we set to null
-+ while ((parking = flushQueue.poll()) != null) {
-+ LockSupport.unpark(parking);
-+ }
-+ }
-+ }
-+
-+ /**
-+ * Notify's this thread that a task has been added to its queue
-+ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
-+ */
-+ public boolean notifyTasks() {
-+ if (this.parked.get() && this.parked.getAndSet(false)) {
-+ LockSupport.unpark(this);
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ protected void queueTask(final T task) {
-+ this.queue.add(task);
-+ this.notifyTasks();
-+ }
-+
-+ /**
-+ * Waits until this thread's queue is empty.
-+ *
-+ * @throws IllegalStateException If the current thread is {@code this} thread.
-+ */
-+ public void flush() {
-+ final Thread currentThread = Thread.currentThread();
-+
-+ if (currentThread == this) {
-+ // avoid deadlock
-+ throw new IllegalStateException("Cannot flush the queue executor thread while on the queue executor thread");
-+ }
-+
-+ // order is important
-+
-+ int successes = 0;
-+ long lastCycle = -1L;
-+
-+ do {
-+ final ConcurrentLinkedQueue flushQueue = this.flushQueue;
-+ if (flushQueue == null) {
-+ return;
-+ }
-+
-+ flushQueue.add(currentThread);
-+
-+ // double check flush queue
-+ if (this.flushQueue == null) {
-+ return;
-+ }
-+
-+ final long currentCycle = this.flushCycles; // may be opaque read
-+
-+ if (currentCycle == lastCycle) {
-+ Thread.yield();
-+ continue;
-+ }
-+
-+ // force response
-+ this.parked.set(false);
-+ LockSupport.unpark(this);
-+
-+ LockSupport.park("flushing queue executor thread");
-+
-+ // returns whether there are tasks queued, does not return whether there are tasks executing
-+ // this is why we cycle twice twice through flush (we know a pollTask call is made after a flush cycle)
-+ // we really only need to guarantee that the tasks this thread has queued has gone through, and can leave
-+ // tasks queued concurrently that are unsychronized with this thread as undefined behavior
-+ if (this.queue.hasTasks()) {
-+ successes = 0;
-+ } else {
-+ ++successes;
-+ }
-+
-+ } while (successes != 2);
-+
-+ }
-+
-+ /**
-+ * 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.
-+ *
-+ *
-+ * This function is MT-Safe.
-+ *
-+ * @param wait If this call is to wait until the queue is empty
-+ * @param killQueue Whether to shutdown this thread's queue
-+ * @return whether this thread shut down the queue
-+ */
-+ public boolean close(final boolean wait, final boolean killQueue) {
-+ boolean ret = !killQueue ? false : this.queue.shutdown();
-+ this.closed = true;
-+
-+ // force thread to respond to the shutdown
-+ this.parked.set(false);
-+ LockSupport.unpark(this);
-+
-+ if (wait) {
-+ this.flush();
-+ }
-+ return ret;
-+ }
-+}
-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import co.aikar.timings.Timing;
-+import com.destroystokyo.paper.io.PaperFileIOThread;
-+import com.destroystokyo.paper.io.IOUtil;
-+import java.util.ArrayDeque;
-+import java.util.function.Consumer;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
-+import org.slf4j.Logger;
-+
-+public final class ChunkLoadTask extends ChunkTask {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ public boolean cancelled;
-+
-+ Consumer onComplete;
-+ public PaperFileIOThread.ChunkData chunkData;
-+
-+ private boolean hasCompleted;
-+
-+ public ChunkLoadTask(final ServerLevel world, final int chunkX, final int chunkZ, final int priority,
-+ final ChunkTaskManager taskManager,
-+ final Consumer onComplete) {
-+ super(world, chunkX, chunkZ, priority, taskManager);
-+ this.onComplete = onComplete;
-+ }
-+
-+ private static final ArrayDeque EMPTY_QUEUE = new ArrayDeque<>();
-+
-+ private static ChunkSerializer.InProgressChunkHolder createEmptyHolder() {
-+ return new ChunkSerializer.InProgressChunkHolder(null, EMPTY_QUEUE);
-+ }
-+
-+ @Override
-+ public void run() {
-+ try {
-+ this.executeTask();
-+ } catch (final Throwable ex) {
-+ LOGGER.error("Failed to execute chunk load task: " + this.toString(), ex);
-+ if (!this.hasCompleted) {
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ }
-+ }
-+ }
-+
-+ private boolean checkCancelled() {
-+ if (this.cancelled) {
-+ // IntelliJ does not understand writes may occur to cancelled concurrently.
-+ return this.taskManager.chunkLoadTasks.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 + ", current: " + ChunkLoadTask.this);
-+ }
-+
-+ if (valueInMap.cancelled) {
-+ return null;
-+ }
-+ return valueInMap;
-+ }) == null;
-+ }
-+ return false;
-+ }
-+
-+ public void executeTask() {
-+ if (this.checkCancelled()) {
-+ return;
-+ }
-+
-+ // either executed synchronously or asynchronously
-+ final PaperFileIOThread.ChunkData chunkData = this.chunkData;
-+
-+ if (chunkData.poiData == PaperFileIOThread.FAILURE_VALUE || chunkData.chunkData == PaperFileIOThread.FAILURE_VALUE) {
-+ LOGGER.error("Could not load chunk for task: " + this.toString() + ", file IO thread has dumped the relevant exception above");
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ return;
-+ }
-+
-+ if (chunkData.chunkData == null) {
-+ // not on disk
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ return;
-+ }
-+
-+ final ChunkPos chunkPos = new ChunkPos(this.chunkX, this.chunkZ);
-+
-+ final ChunkMap chunkManager = this.world.getChunkSource().chunkMap;
-+
-+ try (Timing ignored = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) {
-+ final ChunkSerializer.InProgressChunkHolder chunkHolder;
-+
-+ // apply fixes
-+
-+ try {
-+ chunkData.chunkData = chunkManager.upgradeChunkTag(this.world.getTypeKey(),
-+ chunkManager.overworldDataStorage, chunkData.chunkData, chunkManager.generator.getTypeNameForDataFixer(), chunkPos, this.world); // clone data for safety, file IO thread does not clone
-+ } catch (final Throwable ex) {
-+ LOGGER.error("Could not apply datafixers for chunk task: " + this.toString(), ex);
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ return;
-+ }
-+
-+ if (!ChunkMap.isChunkDataValid(chunkData.chunkData)) {
-+ LOGGER.error("Chunk file at {} is missing level data, skipping", new ChunkPos(this.chunkX, this.chunkZ));
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ return;
-+ }
-+
-+ if (this.checkCancelled()) {
-+ return;
-+ }
-+
-+ try {
-+ chunkHolder = ChunkSerializer.loadChunk(this.world, chunkManager.getPoiManager(), chunkPos,
-+ chunkData.chunkData, true);
-+ } catch (final Throwable ex) {
-+ LOGGER.error("Could not de-serialize chunk data for task: " + this.toString(), ex);
-+ this.complete(ChunkLoadTask.createEmptyHolder());
-+ return;
-+ }
-+
-+ this.complete(chunkHolder);
-+ }
-+ }
-+
-+ private void complete(final ChunkSerializer.InProgressChunkHolder holder) {
-+ this.hasCompleted = true;
-+ holder.poiData = this.chunkData == null ? null : this.chunkData.poiData;
-+
-+ this.taskManager.chunkLoadTasks.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 + ", current: " + ChunkLoadTask.this);
-+ }
-+ if (valueInMap.cancelled) {
-+ return null;
-+ }
-+ try {
-+ ChunkLoadTask.this.onComplete.accept(holder);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
-+ }
-+ return null;
-+ });
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import co.aikar.timings.Timing;
-+import com.destroystokyo.paper.io.PaperFileIOThread;
-+import com.destroystokyo.paper.io.IOUtil;
-+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
-+
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
-+
-+public final class ChunkSaveTask extends ChunkTask {
-+
-+ public final ChunkSerializer.AsyncSaveData asyncSaveData;
-+ public final ChunkAccess chunk;
-+ public final CompletableFuture onComplete = new CompletableFuture<>();
-+
-+ private final AtomicInteger attemptedPriority;
-+
-+ public ChunkSaveTask(final ServerLevel world, final int chunkX, final int chunkZ, final int priority,
-+ final ChunkTaskManager taskManager, final ChunkSerializer.AsyncSaveData asyncSaveData,
-+ final ChunkAccess chunk) {
-+ super(world, chunkX, chunkZ, priority, taskManager);
-+ this.chunk = chunk;
-+ this.asyncSaveData = asyncSaveData;
-+ this.attemptedPriority = new AtomicInteger(priority);
-+ }
-+
-+ @Override
-+ public void run() {
-+ // can be executed asynchronously or synchronously
-+ final CompoundTag compound;
-+
-+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTimingIfSync()) {
-+ compound = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData);
-+ } catch (final Throwable ex) {
-+ // has a plugin modified something it should not have and made us CME?
-+ PaperFileIOThread.LOGGER.error("Failed to serialize unloading chunk data for task: " + this.toString() + ", falling back to a synchronous execution", ex);
-+
-+ // Note: We add to the server thread queue here since this is what the server will drain tasks from
-+ // when waiting for chunks
-+ ChunkTaskManager.queueChunkWaitTask(() -> {
-+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTiming()) {
-+ CompoundTag data = PaperFileIOThread.FAILURE_VALUE;
-+
-+ try {
-+ data = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData);
-+ PaperFileIOThread.LOGGER.info("Successfully serialized chunk data for task: " + this.toString() + " synchronously");
-+ } catch (final Throwable ex1) {
-+ PaperFileIOThread.LOGGER.error("Failed to synchronously serialize unloading chunk data for task: " + this.toString() + "! Chunk data will be lost", ex1);
-+ }
-+
-+ ChunkSaveTask.this.complete(data);
-+ }
-+ });
-+
-+ return; // the main thread will now complete the data
-+ }
-+
-+ this.complete(compound);
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalStateException("Invalid priority: " + priority);
-+ }
-+
-+ // we know priority is valid here
-+ for (int curr = this.attemptedPriority.get();;) {
-+ if (curr <= priority) {
-+ break; // curr is higher/same priority
-+ }
-+ if (this.attemptedPriority.compareAndSet(curr, priority)) {
-+ break;
-+ }
-+ curr = this.attemptedPriority.get();
-+ }
-+
-+ return super.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public boolean updatePriority(final int priority) {
-+ if (!PrioritizedTaskQueue.validPriority(priority)) {
-+ throw new IllegalStateException("Invalid priority: " + priority);
-+ }
-+ this.attemptedPriority.set(priority);
-+ return super.updatePriority(priority);
-+ }
-+
-+ private void complete(final CompoundTag compound) {
-+ try {
-+ this.onComplete.complete(compound);
-+ } catch (final Throwable thr) {
-+ PaperFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
-+ }
-+ if (compound != PaperFileIOThread.FAILURE_VALUE) {
-+ PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, this.chunkX, this.chunkZ, null, compound, this.attemptedPriority.get());
-+ }
-+ this.taskManager.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
-+ if (valueInMap != ChunkSaveTask.this) {
-+ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other: " + valueInMap + ", this: " + ChunkSaveTask.this);
-+ }
-+ return null;
-+ });
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import com.destroystokyo.paper.io.PaperFileIOThread;
-+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
-+import net.minecraft.server.level.ServerLevel;
-+
-+abstract class ChunkTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
-+
-+ public final ServerLevel world;
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final ChunkTaskManager taskManager;
-+
-+ public ChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, final int priority,
-+ final ChunkTaskManager taskManager) {
-+ super(priority);
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.taskManager = taskManager;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "Chunk task: class:" + this.getClass().getName() + ", for world '" + this.world.getWorld().getName() +
-+ "', (" + this.chunkX + "," + this.chunkZ + "), hashcode:" + this.hashCode() + ", priority: " + this.getPriority();
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final int priority) {
-+ PaperFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority);
-+ return super.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public boolean updatePriority(final int priority) {
-+ PaperFileIOThread.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/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import com.destroystokyo.paper.io.PaperFileIOThread;
-+import com.destroystokyo.paper.io.IOUtil;
-+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
-+import com.destroystokyo.paper.io.QueueExecutorThread;
-+import io.papermc.paper.configuration.GlobalConfiguration;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.thread.BlockableEventLoop;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ChunkStatus;
-+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
-+import org.apache.commons.lang.StringUtils;
-+import org.apache.logging.log4j.Level;
-+import org.bukkit.Bukkit;
-+import org.spigotmc.AsyncCatcher;
-+
-+import java.util.ArrayDeque;
-+import java.util.HashSet;
-+import java.util.Set;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.concurrent.ConcurrentLinkedQueue;
-+import java.util.function.Consumer;
-+
-+public final class ChunkTaskManager {
-+
-+ private final QueueExecutorThread[] workers;
-+ private final ServerLevel world;
-+
-+ private final PrioritizedTaskQueue queue;
-+ private final boolean perWorldQueue;
-+
-+ final ConcurrentHashMap chunkLoadTasks = new ConcurrentHashMap<>(64, 0.5f);
-+ final ConcurrentHashMap chunkSaveTasks = new ConcurrentHashMap<>(64, 0.5f);
-+
-+ private final PrioritizedTaskQueue chunkTasks = new PrioritizedTaskQueue<>(); // used if async chunks are disabled in config
-+
-+ protected static QueueExecutorThread[] globalWorkers;
-+ protected static PrioritizedTaskQueue globalQueue;
-+
-+ protected static final ConcurrentLinkedQueue CHUNK_WAIT_QUEUE = new ConcurrentLinkedQueue<>();
-+
-+ public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack
-+
-+ private static final class ChunkInfo {
-+
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final ServerLevel world;
-+
-+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) {
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.world = world;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']";
-+ }
-+ }
-+
-+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) {
-+ synchronized (WAITING_CHUNKS) {
-+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
-+ }
-+ }
-+
-+ public static void popChunkWait() {
-+ synchronized (WAITING_CHUNKS) {
-+ WAITING_CHUNKS.pop();
-+ }
-+ }
-+
-+ private static ChunkInfo[] getChunkInfos() {
-+ ChunkInfo[] chunks;
-+ synchronized (WAITING_CHUNKS) {
-+ chunks = WAITING_CHUNKS.toArray(new ChunkInfo[0]);
-+ }
-+ return chunks;
-+ }
-+
-+ public static void dumpAllChunkLoadInfo() {
-+ ChunkInfo[] chunks = getChunkInfos();
-+ if (chunks.length > 0) {
-+ PaperFileIOThread.LOGGER.error("Chunk wait task info below: ");
-+
-+ for (final ChunkInfo chunkInfo : chunks) {
-+ final long key = IOUtil.getCoordinateKey(chunkInfo.chunkX, chunkInfo.chunkZ);
-+ final ChunkLoadTask loadTask = chunkInfo.world.asyncChunkTaskManager.chunkLoadTasks.get(key);
-+ final ChunkSaveTask saveTask = chunkInfo.world.asyncChunkTaskManager.chunkSaveTasks.get(key);
-+
-+ PaperFileIOThread.LOGGER.error(chunkInfo.chunkX + "," + chunkInfo.chunkZ + " in '" + chunkInfo.world.getWorld().getName() + ":");
-+ PaperFileIOThread.LOGGER.error("Load Task - " + (loadTask == null ? "none" : loadTask.toString()));
-+ PaperFileIOThread.LOGGER.error("Save Task - " + (saveTask == null ? "none" : saveTask.toString()));
-+ // log current status of chunk to indicate whether we're waiting on generation or loading
-+ ChunkHolder chunkHolder = chunkInfo.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(key);
-+
-+ dumpChunkInfo(new HashSet<>(), chunkHolder, chunkInfo.chunkX, chunkInfo.chunkZ);
-+ }
-+ }
-+ }
-+
-+ static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z) {
-+ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1);
-+ }
-+
-+ static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z, int indent, int maxDepth) {
-+ if (seenChunks.contains(chunkHolder)) {
-+ return;
-+ }
-+ if (indent > maxDepth) {
-+ return;
-+ }
-+ seenChunks.add(chunkHolder);
-+ String indentStr = StringUtils.repeat(" ", indent);
-+ if (chunkHolder == null) {
-+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder - null for (" + x +"," + z +")");
-+ } else {
-+ ChunkAccess chunk = chunkHolder.getLastAvailable();
-+ ChunkStatus holderStatus = chunkHolder.getChunkHolderStatus();
-+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder - non-null");
-+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getStatus().toString()));
-+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Ticket Status - " + ChunkHolder.getStatus(chunkHolder.getTicketLevel()));
-+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString()));
-+ }
-+ }
-+
-+ public static void processConfiguration(GlobalConfiguration.AsyncChunks config) {
-+ int threads = config.threads; // don't write back to config
-+ int cpus = Runtime.getRuntime().availableProcessors() / 2;
-+ if (threads <= 0) {
-+ if (cpus <= 4) {
-+ threads = cpus <= 2 ? 1 : 2;
-+ } else {
-+ threads = (int) Math.min(Integer.getInteger("paper.maxChunkThreads", 4), cpus / 2);
-+ }
-+ }
-+ if (cpus == 1 && !Boolean.getBoolean("Paper.allowAsyncChunksSingleCore")) {
-+ config.asyncChunks = false;
-+ } else {
-+ config.asyncChunks = true;
-+ }
-+
-+ // Let Shared Host set some limits
-+ String sharedHostThreads = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_THREADS");
-+ if (sharedHostThreads != null) {
-+ try {
-+ threads = Math.max(1, Math.min(threads, Integer.parseInt(sharedHostThreads)));
-+ } catch (NumberFormatException ignored) {}
-+ }
-+
-+ if (config.asyncChunks) {
-+ ChunkTaskManager.initGlobalLoadThreads(threads);
-+ }
-+ }
-+
-+ public static void initGlobalLoadThreads(int threads) {
-+ if (threads <= 0 || globalWorkers != null) {
-+ return;
-+ }
-+ ++threads; // add one for urgent executor
-+
-+ globalWorkers = new QueueExecutorThread[threads];
-+ globalQueue = new PrioritizedTaskQueue<>();
-+
-+ for (int i = 0; i < (threads - 1); ++i) {
-+ globalWorkers[i] = new QueueExecutorThread<>(globalQueue, (long)0.10e6); //0.1ms
-+ globalWorkers[i].setName("Paper Async Chunk Task Thread #" + i);
-+ globalWorkers[i].setPriority(Thread.NORM_PRIORITY - 1);
-+ globalWorkers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
-+ PaperFileIOThread.LOGGER.error("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
-+ });
-+
-+ globalWorkers[i].start();
-+ }
-+
-+ globalWorkers[threads - 1] = new QueueExecutorThread<>(globalQueue, (long)0.10e6); //0.1ms
-+ globalWorkers[threads - 1].setName("Paper Async Chunk Urgent Task Thread");
-+ globalWorkers[threads - 1].setPriority(Thread.NORM_PRIORITY+1);
-+ globalWorkers[threads - 1].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
-+ PaperFileIOThread.LOGGER.error("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
-+ });
-+ globalWorkers[threads - 1].setLowestPriorityToPoll(PrioritizedTaskQueue.HIGHEST_PRIORITY);
-+ globalWorkers[threads - 1].start();
-+ }
-+
-+ /**
-+ * Creates this chunk task manager to operate off the specified number of threads. If the specified number of threads is
-+ * less-than or equal to 0, then this chunk task manager will operate off of the world's chunk task queue.
-+ * @param world Specified world.
-+ * @param threads Specified number of threads.
-+ * @see ServerChunkCache#mainThreadProcessor
-+ */
-+ public ChunkTaskManager(final ServerLevel world, final int threads) {
-+ this.world = world;
-+ this.workers = threads <= 0 ? null : new QueueExecutorThread[threads];
-+ this.queue = new PrioritizedTaskQueue<>();
-+ this.perWorldQueue = true;
-+
-+ 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 #" + i + " for world: " + world.getWorld().getName());
-+ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1);
-+ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
-+ PaperFileIOThread.LOGGER.error("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
-+ });
-+
-+ this.workers[i].start();
-+ }
-+ }
-+
-+ /**
-+ * Creates the chunk task manager to work from the global workers. When {@link #close(boolean)} is invoked,
-+ * the global queue is not shutdown. If the global workers is configured to be disabled or use 0 threads, then
-+ * this chunk task manager will operate off of the world's chunk task queue.
-+ * @param world The world that this task manager is responsible for
-+ * @see ServerChunkCache#mainThreadProcessor
-+ */
-+ public ChunkTaskManager(final ServerLevel world) {
-+ this.world = world;
-+ this.workers = globalWorkers;
-+ this.queue = globalQueue;
-+ this.perWorldQueue = false;
-+ }
-+
-+ public boolean pollNextChunkTask() {
-+ final ChunkTask task = this.chunkTasks.poll();
-+
-+ if (task != null) {
-+ task.run();
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ /**
-+ * Polls and runs the next available chunk wait queue task. This is to be used when the server is waiting on a chunk queue.
-+ * (per-world can cause issues if all the worker threads are blocked waiting for a response from the main thread)
-+ */
-+ public static boolean pollChunkWaitQueue() {
-+ final Runnable run = CHUNK_WAIT_QUEUE.poll();
-+ if (run != null) {
-+ run.run();
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ /**
-+ * Queues a chunk wait task. Note that this will execute out of order with respect to tasks scheduled on a world's
-+ * chunk task queue, since this is the global chunk wait queue.
-+ */
-+ public static void queueChunkWaitTask(final Runnable runnable) {
-+ CHUNK_WAIT_QUEUE.add(runnable);
-+ }
-+
-+ private static void drainChunkWaitQueue() {
-+ Runnable run;
-+ while ((run = CHUNK_WAIT_QUEUE.poll()) != null) {
-+ run.run();
-+ }
-+ }
-+
-+ /**
-+ * The exact same as {@link #scheduleChunkLoad(int, int, int, Consumer, boolean)}, except that the chunk data is provided as
-+ * the {@code data} parameter.
-+ */
-+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
-+ final Consumer onComplete,
-+ final boolean intendingToBlock, final CompletableFuture dataFuture) {
-+ final ServerLevel world = this.world;
-+
-+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
-+ if (valueInMap != null) {
-+ if (!valueInMap.cancelled) {
-+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
-+ }
-+ valueInMap.cancelled = false;
-+ valueInMap.onComplete = onComplete;
-+ return valueInMap;
-+ }
-+
-+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
-+
-+ dataFuture.thenAccept((final CompoundTag data) -> {
-+ final boolean failed = data == PaperFileIOThread.FAILURE_VALUE;
-+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
-+ ret.chunkData = chunkData;
-+ if (!failed) {
-+ chunkData.chunkData = data;
-+ }
-+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
-+ }, true, failed, intendingToBlock); // read data off disk if the future fails
-+ });
-+
-+ return ret;
-+ });
-+ }
-+
-+ public void cancelChunkLoad(final int chunkX, final int chunkZ) {
-+ this.chunkLoadTasks.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ return null;
-+ }
-+
-+ if (valueInMap.cancelled) {
-+ PaperFileIOThread.LOGGER.warn("Task " + valueInMap.toString() + " is already cancelled!");
-+ }
-+ valueInMap.cancelled = true;
-+ if (valueInMap.cancel()) {
-+ return null;
-+ }
-+
-+ return valueInMap;
-+ });
-+ }
-+
-+ /**
-+ * Schedules an asynchronous chunk load for the specified coordinates. The onComplete parameter may be invoked asynchronously
-+ * on a worker thread or on the world's chunk executor queue. As such the code that is executed for the parameter should be
-+ * carefully chosen.
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param priority Priority for this task
-+ * @param onComplete The consumer to invoke with the {@link ChunkSerializer.InProgressChunkHolder} object once this task is complete
-+ * @param intendingToBlock Whether the caller is intending to block on this task completing (this is a performance tune, and has no adverse side-effects)
-+ * @return The {@link ChunkLoadTask} associated with
-+ */
-+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
-+ final Consumer onComplete,
-+ final boolean intendingToBlock) {
-+ final ServerLevel world = this.world;
-+
-+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
-+ if (valueInMap != null) {
-+ if (!valueInMap.cancelled) {
-+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
-+ }
-+ valueInMap.cancelled = false;
-+ valueInMap.onComplete = onComplete;
-+ return valueInMap;
-+ }
-+
-+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
-+
-+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
-+ ret.chunkData = chunkData;
-+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
-+ }, true, true, intendingToBlock);
-+
-+ return ret;
-+ });
-+ }
-+
-+ /**
-+ * Schedules an async save for the specified chunk. The chunk, at the beginning of this call, must be completely unloaded
-+ * from the world.
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param priority Priority for this task
-+ * @param asyncSaveData Async save data. See {@link ChunkSerializer#getAsyncSaveData(ServerLevel, ChunkAccess)}
-+ * @param chunk Chunk to save
-+ * @return The {@link ChunkSaveTask} associated with the save task.
-+ */
-+ public ChunkSaveTask scheduleChunkSave(final int chunkX, final int chunkZ, final int priority,
-+ final ChunkSerializer.AsyncSaveData asyncSaveData,
-+ final ChunkAccess chunk) {
-+ AsyncCatcher.catchOp("chunk save schedule");
-+
-+ final ServerLevel world = this.world;
-+
-+ return this.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
-+ if (valueInMap != null) {
-+ throw new IllegalStateException("Double scheduling chunk save for task: " + valueInMap.toString());
-+ }
-+
-+ final ChunkSaveTask ret = new ChunkSaveTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, asyncSaveData, chunk);
-+
-+ ChunkTaskManager.this.internalSchedule(ret);
-+
-+ return ret;
-+ });
-+ }
-+
-+ /**
-+ * Returns a completable future which will be completed with the un-copied chunk data for an in progress async save.
-+ * Returns {@code null} if no save is in progress.
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ */
-+ public CompletableFuture getChunkSaveFuture(final int chunkX, final int chunkZ) {
-+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
-+ if (chunkSaveTask == null) {
-+ return null;
-+ }
-+ return chunkSaveTask.onComplete;
-+ }
-+
-+ /**
-+ * Returns the chunk object being used to serialize data async for an unloaded chunk. Note that modifying this chunk
-+ * is not safe to do as another thread is handling its save. The chunk is also not loaded into the world.
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @return Chunk object for an in-progress async save, or {@code null} if no save is in progress
-+ */
-+ public ChunkAccess getChunkInSaveProgress(final int chunkX, final int chunkZ) {
-+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
-+ if (chunkSaveTask == null) {
-+ return null;
-+ }
-+ return chunkSaveTask.chunk;
-+ }
-+
-+ public void flush() {
-+ // flush here since we schedule tasks on the IO thread that can schedule tasks here
-+ drainChunkWaitQueue();
-+ PaperFileIOThread.Holder.INSTANCE.flush();
-+ drainChunkWaitQueue();
-+
-+ if (this.workers == null) {
-+ if (Bukkit.isPrimaryThread() || MinecraftServer.getServer().hasStopped()) {
-+ ((BlockableEventLoop)this.world.getChunkSource().mainThreadProcessor).runAllTasks();
-+ } else {
-+ CompletableFuture wait = new CompletableFuture<>();
-+ MinecraftServer.getServer().scheduleOnMain(() -> {
-+ ((BlockableEventLoop)this.world.getChunkSource().mainThreadProcessor).runAllTasks();
-+ });
-+ wait.join();
-+ }
-+ } else {
-+ for (final QueueExecutorThread worker : this.workers) {
-+ worker.flush();
-+ }
-+ }
-+
-+ // flush again since tasks we execute async saves
-+ drainChunkWaitQueue();
-+ PaperFileIOThread.Holder.INSTANCE.flush();
-+ }
-+
-+ public void close(final boolean wait) {
-+ // flush here since we schedule tasks on the IO thread that can schedule tasks to this task manager
-+ // we do this regardless of the wait param since after we invoke close no tasks can be queued
-+ PaperFileIOThread.Holder.INSTANCE.flush();
-+
-+ if (this.workers == null) {
-+ if (wait) {
-+ this.flush();
-+ }
-+ return;
-+ }
-+
-+ if (this.workers != globalWorkers) {
-+ for (final QueueExecutorThread worker : this.workers) {
-+ worker.close(false, this.perWorldQueue);
-+ }
-+ }
-+
-+ if (wait) {
-+ this.flush();
-+ }
-+ }
-+
-+ public void raisePriority(final int chunkX, final int chunkZ, final int priority) {
-+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
-+
-+ ChunkTask chunkSaveTask = this.chunkSaveTasks.get(chunkKey);
-+ if (chunkSaveTask != null) {
-+ // don't bump save into urgent queue
-+ raiseTaskPriority(chunkSaveTask, priority != PrioritizedTaskQueue.HIGHEST_PRIORITY ? priority : PrioritizedTaskQueue.HIGH_PRIORITY);
-+ }
-+
-+ ChunkLoadTask chunkLoadTask = this.chunkLoadTasks.get(chunkKey);
-+ if (chunkLoadTask != null) {
-+ raiseTaskPriority(chunkLoadTask, priority);
-+ }
-+ }
-+
-+ private void raiseTaskPriority(ChunkTask task, int priority) {
-+ final boolean raised = task.raisePriority(priority);
-+ if (task.isScheduled() && raised && this.workers != null) {
-+ // only notify if we're in queue to be executed
-+ if (priority == PrioritizedTaskQueue.HIGHEST_PRIORITY) {
-+ // notify urgent worker as well
-+ this.internalScheduleNotifyUrgent();
-+ }
-+ this.internalScheduleNotify();
-+ }
-+ }
-+
-+ protected void internalSchedule(final ChunkTask task) {
-+ if (this.workers == null) {
-+ this.chunkTasks.add(task);
-+ return;
-+ }
-+
-+ // It's important we order the task to be executed before notifying. Avoid a race condition where the worker thread
-+ // wakes up and goes to sleep before we actually schedule (or it's just about to sleep)
-+ this.queue.add(task);
-+ this.internalScheduleNotify();
-+ if (task.getPriority() == PrioritizedTaskQueue.HIGHEST_PRIORITY) {
-+ // notify urgent too
-+ this.internalScheduleNotifyUrgent();
-+ }
-+
-+ }
-+
-+ protected void internalScheduleNotify() {
-+ if (this.workers == null) {
-+ return;
-+ }
-+ for (int i = 0, len = this.workers.length - 1; i < len; ++i) {
-+ final QueueExecutorThread worker = this.workers[i];
-+ if (worker.notifyTasks()) {
-+ // break here since we only want to wake up one worker for scheduling one task
-+ break;
-+ }
-+ }
-+ }
-+
-+
-+ protected void internalScheduleNotifyUrgent() {
-+ if (this.workers == null) {
-+ return;
-+ }
-+ this.workers[this.workers.length - 1].notifyTasks();
-+ }
-+
-+}
-diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
-@@ -0,0 +0,0 @@ public class ServerboundCommandSuggestionPacket implements Packet {
- DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, config.get(), ops.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::new);
-
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
-+ if (++saved[0] >= maxAsyncSaves) {
-+ saved[0] = 0;
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.flush();
-+ }
-+ };
-+ // Paper end - do not overload I/O threads with too much work when saving
- if (flush) {
- List list = (List) net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
- MutableBoolean mutableboolean = new MutableBoolean();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }).filter((ichunkaccess) -> {
- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
- }).filter(this::save).forEach((ichunkaccess) -> {
-+ onChunkSave.run(); // Paper - do not overload I/O threads with too much work when saving
- mutableboolean.setTrue();
- });
- } while (mutableboolean.isTrue());
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.processUnloads(() -> {
- return true;
- });
-- this.flushWorker();
-+ //this.flushWorker(); // Paper - nuke IOWorker
-+ this.level.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
- } else {
- net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
- }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- protected void tick(BooleanSupplier shouldKeepTicking) {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
-+ try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper
- gameprofilerfiller.push("poi");
- this.poiManager.tick(shouldKeepTicking);
-+ } // Paper
- gameprofilerfiller.popPush("chunk_unload");
- if (!this.level.noSave()) {
-+ try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper
- this.processUnloads(shouldKeepTicking);
-+ } // Paper
- }
-
- gameprofilerfiller.pop();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- ((LevelChunk) ichunkaccess).setLoaded(false);
- }
-
-- this.save(ichunkaccess);
-+ // Paper start - async chunk saving
-+ try {
-+ this.asyncSave(ichunkaccess);
-+ } catch (ThreadDeath ex) {
-+ throw ex; // bye
-+ } catch (Throwable ex) {
-+ LOGGER.error("Failed to prepare async save, attempting synchronous save", ex);
-+ this.save(ichunkaccess);
-+ }
-+ // Paper end - async chunk saving
- if (this.entitiesInLevel.remove(pos) && ichunkaccess instanceof LevelChunk) {
- LevelChunk chunk = (LevelChunk) ichunkaccess;
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- private CompletableFuture> scheduleChunkLoad(ChunkPos pos) {
-- return this.readChunk(pos).thenApply((optional) -> {
-- return optional.filter((nbttagcompound) -> {
-- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound);
-+ // Paper start - Async chunk io
-+ final java.util.function.BiFunction> syncLoadComplete = (chunkHolder, ioThrowable) -> {
-+ try (Timing ignored = this.level.timings.chunkLoad.startTimingIfSync()) { // Paper
-+ this.level.getProfiler().incrementCounter("chunkLoad");
-+ if (ioThrowable != null) {
-+ return this.handleChunkLoadFailure(ioThrowable, pos);
-+ }
-+ this.poiManager.loadInData(pos, chunkHolder.poiData);
-+ chunkHolder.tasks.forEach(Runnable::run);
-
-- if (!flag) {
-- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos);
-+ if (chunkHolder.protoChunk != null) {
-+ ProtoChunk protochunk = chunkHolder.protoChunk;
-+ this.markPosition(pos, protochunk.getStatus().getChunkType());
-+ return Either.left(protochunk);
- }
-+ } catch (Exception ex) {
-+ return this.handleChunkLoadFailure(ex, pos);
-+ }
-
-- return flag;
-+ return Either.left(this.createEmptyChunk(pos));
-+ };
-+ CompletableFuture> ret = new CompletableFuture<>();
-+
-+ Consumer chunkHolderConsumer = (ChunkSerializer.InProgressChunkHolder holder) -> {
-+ // Go into the chunk load queue and not server task queue so we can be popped out even faster.
-+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.queueChunkWaitTask(() -> {
-+ try {
-+ ret.complete(syncLoadComplete.apply(holder, null));
-+ } catch (Exception e) {
-+ ret.completeExceptionally(e);
-+ }
- });
-- }).thenApplyAsync((optional) -> {
-- this.level.getProfiler().incrementCounter("chunkLoad");
-- if (optional.isPresent()) {
-- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, pos, (CompoundTag) optional.get());
-+ };
-
-- this.markPosition(pos, protochunk.getStatus().getChunkType());
-- return Either.left(protochunk); // CraftBukkit - decompile error
-- } else {
-- return Either.left(this.createEmptyChunk(pos)); // CraftBukkit - decompile error
-- }
-- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
-- return this.handleChunkLoadFailure(throwable, pos);
-- }, this.mainThreadExecutor);
-+ CompletableFuture chunkSaveFuture = this.level.asyncChunkTaskManager.getChunkSaveFuture(pos.x, pos.z);
-+ if (chunkSaveFuture != null) {
-+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
-+ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture);
-+ this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY);
-+ } else {
-+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z,
-+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false);
-+ }
-+ return ret;
-+ // Paper end - Async chunk io
- }
-
-- private static boolean isChunkDataValid(CompoundTag nbt) {
-+ public static boolean isChunkDataValid(CompoundTag nbt) { // Paper - async chunk loading
- return nbt.contains("Status", 8);
- }
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- }
-
-+ // Paper start - async chunk save for unload
-+ // Note: This is very unsafe to call if the chunk is still in use.
-+ // This is also modeled after PlayerChunkMap#save(IChunkAccess, boolean), with the intentional difference being
-+ // serializing the chunk is left to a worker thread.
-+ private void asyncSave(ChunkAccess chunk) {
-+ ChunkPos chunkPos = chunk.getPos();
-+ CompoundTag poiData;
-+ try (Timing ignored = this.level.timings.chunkUnloadPOISerialization.startTiming()) {
-+ poiData = this.poiManager.getData(chunk.getPos());
-+ }
-+
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.level, chunkPos.x, chunkPos.z,
-+ poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
-+
-+ if (!chunk.isUnsaved()) {
-+ return;
-+ }
-+
-+ ChunkStatus chunkstatus = chunk.getStatus();
-+
-+ // Copied from PlayerChunkMap#save(IChunkAccess, boolean)
-+ if (chunkstatus.getChunkType() != ChunkStatus.ChunkType.LEVELCHUNK) {
-+ // Paper start - Optimize save by using status cache
-+ if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
-+ return;
-+ }
-+ }
-+
-+ ChunkSerializer.AsyncSaveData asyncSaveData;
-+ try (Timing ignored = this.level.timings.chunkUnloadPrepareSave.startTiming()) {
-+ asyncSaveData = ChunkSerializer.getAsyncSaveData(this.level, chunk);
-+ }
-+
-+ this.level.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY,
-+ asyncSaveData, chunk);
-+
-+ chunk.setUnsaved(false);
-+ }
-+ // Paper end
-+
- public boolean save(ChunkAccess chunk) {
-+ try (co.aikar.timings.Timing ignored = this.level.timings.chunkSave.startTiming()) { // Paper
- this.poiManager.flush(chunk.getPos());
- if (!chunk.isUnsaved()) {
- return false;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- ChunkStatus chunkstatus = chunk.getStatus();
-
- if (chunkstatus.getChunkType() != ChunkStatus.ChunkType.LEVELCHUNK) {
-- if (this.isExistingChunkFull(chunkcoordintpair)) {
-+ if (false && this.isExistingChunkFull(chunkcoordintpair)) { // Paper
- return false;
- }
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- this.level.getProfiler().incrementCounter("chunkSave");
-- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk);
-+ CompoundTag nbttagcompound;
-+ try (co.aikar.timings.Timing ignored1 = this.level.timings.chunkSaveDataSerialization.startTiming()) { // Paper
-+ nbttagcompound = ChunkSerializer.write(this.level, chunk);
-+ } // Paper
-
-- this.write(chunkcoordintpair, nbttagcompound);
-+ // Paper start - async chunk io
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.level, chunkcoordintpair.x, chunkcoordintpair.z,
-+ null, nbttagcompound, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
-+ // Paper end - async chunk io
- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
- return true;
- } catch (Exception exception) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- return false;
- }
- }
-+ } // Paper
- }
-
- private boolean isExistingChunkFull(ChunkPos pos) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- }
-
-+ // Paper start - Asynchronous chunk io
-+ @Nullable
-+ @Override
-+ public CompoundTag readSync(ChunkPos chunkcoordintpair) throws IOException {
-+ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
-+ CompoundTag ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.level, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ false, true, true).join().chunkData;
-+
-+ if (ret == com.destroystokyo.paper.io.PaperFileIOThread.FAILURE_VALUE) {
-+ throw new IOException("See logs for further detail");
-+ }
-+ return ret;
-+ }
-+ return super.readSync(chunkcoordintpair);
-+ }
-+
-+ @Override
-+ public void write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException {
-+ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.level, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
-+ return;
-+ }
-+ super.write(chunkcoordintpair, nbttagcompound);
-+ }
-+ // Paper end
-+
- private CompletableFuture> readChunk(ChunkPos chunkPos) {
- return this.read(chunkPos).thenApplyAsync((optional) -> {
- return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
- }
-
- public void removeTicketsOnClosing() {
-- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
-+ ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.ASYNC_LOAD); // Paper - add additional tickets to preserve
- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- return ret;
- }
- // Paper end
-+ // Paper start - async chunk io
-+ public CompletableFuture> getChunkAtAsynchronously(int x, int z, boolean gen, boolean isUrgent) {
-+ CompletableFuture> ret = new CompletableFuture<>();
-+
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority;
-+ if (isUrgent) {
-+ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER;
-+ } else {
-+ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL;
-+ }
-+
-+ net.minecraft.server.ChunkSystem.scheduleChunkLoad(this.level, x, z, gen, ChunkStatus.FULL, true, priority, (chunk) -> {
-+ if (chunk == null) {
-+ ret.complete(ChunkHolder.UNLOADED_CHUNK);
-+ } else {
-+ ret.complete(Either.left(chunk));
-+ }
-+ });
-+
-+ return ret;
-+ }
-+ // Paper end - async chunk io
-
- @Nullable
- @Override
- public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
-+ final int x1 = x; final int z1 = z; // Paper - conflict on variable change
- if (Thread.currentThread() != this.mainThread) {
- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
- return this.getChunk(x, z, leastStatus, create);
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
-- CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
-+ CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create, true); // Paper
- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
-
- Objects.requireNonNull(completablefuture);
- if (!completablefuture.isDone()) { // Paper
-+ // Paper start - async chunk io/loading
-+ this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
-+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1);
-+ // Paper end
- this.level.timings.syncChunkLoad.startTiming(); // Paper
- chunkproviderserver_b.managedBlock(completablefuture::isDone);
-+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
- this.level.timings.syncChunkLoad.stopTiming(); // Paper
- } // Paper
- ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> {
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
-+ // Paper start - add isUrgent - old sig left in place for dirty nms plugins
-+ return getChunkFutureMainThread(chunkX, chunkZ, leastStatus, create, false);
-+ }
-+ private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create, boolean isUrgent) {
-+ // Paper end
- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
- long k = chunkcoordintpair.toLong();
- int l = 33 + ChunkStatus.getDistance(leastStatus);
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
- public boolean pollTask() {
- try {
-+ boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper
- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
- return true;
- } else {
- ServerChunkCache.this.lightEngine.tryScheduleUpdate();
-- return super.pollTask();
-+ return super.pollTask() || execChunkTask; // Paper
- }
- } finally {
- chunkMap.callbackExecutor.run();
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
- }
- }
- }
-+
-+ // Paper start - Asynchronous IO
-+ public final com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController poiDataController = new com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController() {
-+ @Override
-+ public void writeData(int x, int z, net.minecraft.nbt.CompoundTag compound) throws java.io.IOException {
-+ ServerLevel.this.getChunkSource().chunkMap.getPoiManager().write(new ChunkPos(x, z), compound);
-+ }
-+
-+ @Override
-+ public net.minecraft.nbt.CompoundTag readData(int x, int z) throws java.io.IOException {
-+ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager().read(new ChunkPos(x, z));
-+ }
-+
-+ @Override
-+ public T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function function) {
-+ synchronized (ServerLevel.this.getChunkSource().chunkMap.getPoiManager()) {
-+ net.minecraft.world.level.chunk.storage.RegionFile file;
-+
-+ try {
-+ file = ServerLevel.this.getChunkSource().chunkMap.getPoiManager().getRegionFile(new ChunkPos(chunkX, chunkZ), false);
-+ } catch (java.io.IOException ex) {
-+ throw new RuntimeException(ex);
-+ }
-+
-+ return function.apply(file);
-+ }
-+ }
-+
-+ @Override
-+ public T computeForRegionFileIfLoaded(int chunkX, int chunkZ, java.util.function.Function function) {
-+ synchronized (ServerLevel.this.getChunkSource().chunkMap.getPoiManager()) {
-+ net.minecraft.world.level.chunk.storage.RegionFile file = ServerLevel.this.getChunkSource().chunkMap.getPoiManager().getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ));
-+ return function.apply(file);
-+ }
-+ }
-+ };
-+
-+ public final com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController chunkDataController = new com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController() {
-+ @Override
-+ public void writeData(int x, int z, net.minecraft.nbt.CompoundTag compound) throws java.io.IOException {
-+ ServerLevel.this.getChunkSource().chunkMap.write(new ChunkPos(x, z), compound);
-+ }
-+
-+ @Override
-+ public net.minecraft.nbt.CompoundTag readData(int x, int z) throws java.io.IOException {
-+ return ServerLevel.this.getChunkSource().chunkMap.readSync(new ChunkPos(x, z));
-+ }
-+
-+ @Override
-+ public T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function function) {
-+ synchronized (ServerLevel.this.getChunkSource().chunkMap) {
-+ net.minecraft.world.level.chunk.storage.RegionFile file;
-+
-+ try {
-+ file = ServerLevel.this.getChunkSource().chunkMap.regionFileCache.getRegionFile(new ChunkPos(chunkX, chunkZ), false);
-+ } catch (java.io.IOException ex) {
-+ throw new RuntimeException(ex);
-+ }
-+
-+ return function.apply(file);
-+ }
-+ }
-+
-+ @Override
-+ public T computeForRegionFileIfLoaded(int chunkX, int chunkZ, java.util.function.Function function) {
-+ synchronized (ServerLevel.this.getChunkSource().chunkMap) {
-+ net.minecraft.world.level.chunk.storage.RegionFile file = ServerLevel.this.getChunkSource().chunkMap.regionFileCache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ));
-+ return function.apply(file);
-+ }
-+ }
-+ };
-+ public final com.destroystokyo.paper.io.chunk.ChunkTaskManager asyncChunkTaskManager;
- // Paper end
-
- // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
-@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
-
- this.sleepStatus = new SleepStatus();
- this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
-+
-+ this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper
- }
-
- public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) {
-diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/TicketType.java
-+++ b/src/main/java/net/minecraft/server/level/TicketType.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
-
- public class TicketType {
- public static final TicketType FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
-+ public static final TicketType ASYNC_LOAD = create("async_load", Long::compareTo); // Paper
-
- private final String name;
- private final Comparator comparator;
-diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
- this.disconnect(Component.translatable("disconnect.spam"));
- return;
- }
-+ // Paper start
-+ String str = packet.getCommand(); int index = -1;
-+ if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
-+ server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]))); // Paper
-+ return;
-+ }
-+ // Paper end
- // CraftBukkit end
- StringReader stringreader = new StringReader(packet.getCommand());
-
-diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-@@ -0,0 +0,0 @@ public class WorldUpgrader {
- }
-
- WorldUpgrader.LOGGER.error("Error upgrading chunk {}", chunkcoordintpair, throwable);
-+ // Paper start
-+ } catch (IOException e) {
-+ WorldUpgrader.LOGGER.error("Error upgrading chunk {}", chunkcoordintpair, e);
- }
-+ // Paper end
-
- if (flag1) {
- ++this.converted;
-diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
- public static final int VILLAGE_SECTION_SIZE = 1;
- private final PoiManager.DistanceTracker distanceTracker;
- private final LongSet loadedChunks = new LongOpenHashSet();
-+ private final net.minecraft.server.level.ServerLevel world; // Paper
-
- public PoiManager(Path path, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world) {
- super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, registryManager, world);
-+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper
- this.distanceTracker = new PoiManager.DistanceTracker();
- }
-
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
-
- @Override
- public void tick(BooleanSupplier shouldKeepTicking) {
-- super.tick(shouldKeepTicking);
-+ // Paper start - async chunk io
-+ while (!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean()) {
-+ ChunkPos chunkcoordintpair = SectionPos.of(this.dirty.firstLong()).chunk();
-+
-+ net.minecraft.nbt.CompoundTag data;
-+ try (co.aikar.timings.Timing ignored1 = this.world.timings.poiSaveDataSerialization.startTiming()) {
-+ data = this.getData(chunkcoordintpair);
-+ }
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
-+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
-+ }
-+ // Paper end
- this.distanceTracker.runAllUpdates();
- }
-
-@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage {
- }
- }
-
-+ // Paper start - Asynchronous chunk io
-+ @javax.annotation.Nullable
-+ @Override
-+ public net.minecraft.nbt.CompoundTag read(ChunkPos chunkcoordintpair) throws java.io.IOException {
-+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
-+ net.minecraft.nbt.CompoundTag ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ true, false, true).join().poiData;
-+
-+ if (ret == com.destroystokyo.paper.io.PaperFileIOThread.FAILURE_VALUE) {
-+ throw new java.io.IOException("See logs for further detail");
-+ }
-+ return ret;
-+ }
-+ return super.read(chunkcoordintpair);
-+ }
-+
-+ @Override
-+ public void write(ChunkPos chunkcoordintpair, net.minecraft.nbt.CompoundTag nbttagcompound) throws java.io.IOException {
-+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
-+ return;
-+ }
-+ super.write(chunkcoordintpair, nbttagcompound);
-+ }
-+ // Paper end
-+
- public static enum Occupancy {
- HAS_SPACE(PoiRecord::hasSpace),
- IS_OCCUPIED(PoiRecord::isOccupied),
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
-
- public ChunkSerializer() {}
-
-+ // Paper start
-+ public static final class InProgressChunkHolder {
-+
-+ public final ProtoChunk protoChunk;
-+ public final java.util.ArrayDeque tasks;
-+
-+ public CompoundTag poiData;
-+
-+ public InProgressChunkHolder(final ProtoChunk protoChunk, final java.util.ArrayDeque tasks) {
-+ this.protoChunk = protoChunk;
-+ this.tasks = tasks;
-+ }
-+ }
-+ // Paper end
-+
- public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) {
-+ // Paper start - add variant for async calls
-+ InProgressChunkHolder holder = loadChunk(world, poiStorage, chunkPos, nbt, true);
-+ holder.tasks.forEach(Runnable::run);
-+ return holder.protoChunk;
-+ }
-+
-+ public static InProgressChunkHolder loadChunk(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt, boolean distinguish) {
-+ java.util.ArrayDeque tasksToExecuteOnMain = new java.util.ArrayDeque<>();
-+ // Paper end
- ChunkPos chunkcoordintpair1 = new ChunkPos(nbt.getInt("xPos"), nbt.getInt("zPos"));
-
- if (!Objects.equals(chunkPos, chunkcoordintpair1)) {
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- LevelChunkSection chunksection = new LevelChunkSection(b0, datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write
-
- achunksection[k] = chunksection;
-+ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
- poiStorage.checkConsistencyWithBlocks(chunkPos, chunksection);
-+ }); // Paper - delay this task since we're executing off-main
- }
-
- boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
-
- if (flag3 || flag4) {
- if (!flag2) {
-+ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
- lightengine.retainData(chunkPos, true);
-+ }); // Paper - delay this task since we're executing off-main
- flag2 = true;
- }
-
- if (flag3) {
-- lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight")), true);
-+ // Paper start - delay this task since we're executing off-main
-+ DataLayer blockLight = new DataLayer(nbttagcompound1.getByteArray("BlockLight").clone());
-+ tasksToExecuteOnMain.add(() -> {
-+ lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), blockLight, true);
-+ });
-+ // Paper end - delay this task since we're executing off-main
- }
-
- if (flag4) {
-- lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight")), true);
-+ // Paper start - delay this task since we're executing off-main
-+ DataLayer skyLight = new DataLayer(nbttagcompound1.getByteArray("SkyLight").clone());
-+ tasksToExecuteOnMain.add(() -> {
-+ lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), skyLight, true);
-+ });
-+ // Paper end - delay this task since we're executing off-mai
- }
- }
- }
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- }
-
- if (chunkstatus_type == ChunkStatus.ChunkType.LEVELCHUNK) {
-- return new ImposterProtoChunk((LevelChunk) object1, false);
-+ return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false), tasksToExecuteOnMain); // Paper - Async chunk loading
- } else {
- ProtoChunk protochunk1 = (ProtoChunk) object1;
-
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- protochunk1.setCarvingMask(worldgenstage_features, new CarvingMask(nbttagcompound4.getLongArray(s1), ((ChunkAccess) object1).getMinBuildHeight()));
- }
-
-- return protochunk1;
-+ return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading
-+ }
-+ }
-+
-+ // Paper start - async chunk save for unload
-+ public record AsyncSaveData(
-+ DataLayer[] blockLight,
-+ DataLayer[] skyLight,
-+ Tag blockTickList, // non-null if we had to go to the server's tick list
-+ Tag fluidTickList, // non-null if we had to go to the server's tick list
-+ ListTag blockEntities,
-+ long worldTime
-+ ) {}
-+
-+ // must be called sync
-+ public static AsyncSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
-+ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save");
-+ ChunkPos chunkPos = chunk.getPos();
-+
-+ ThreadedLevelLightEngine lightenginethreaded = world.getChunkSource().getLightEngine();
-+
-+ DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()];
-+ DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()];
-+
-+ for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
-+ DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i));
-+ DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i));
-+
-+ // copy data for safety
-+ if (blockArray != null) {
-+ blockArray = blockArray.copy();
-+ }
-+ if (skyArray != null) {
-+ skyArray = skyArray.copy();
-+ }
-+
-+ blockLight[i - lightenginethreaded.getMinLightSection()] = blockArray;
-+ skyLight[i - lightenginethreaded.getMinLightSection()] = skyArray;
-+ }
-+
-+ final CompoundTag tickLists = new CompoundTag();
-+ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization());
-+
-+ ListTag blockEntitiesSerialized = new ListTag();
-+ for (final BlockPos blockPos : chunk.getBlockEntitiesPos()) {
-+ final CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos);
-+ if (blockEntityNbt != null) {
-+ blockEntitiesSerialized.add(blockEntityNbt);
-+ }
- }
-+
-+ return new AsyncSaveData(
-+ blockLight,
-+ skyLight,
-+ tickLists.get(BLOCK_TICKS_TAG),
-+ tickLists.get(FLUID_TICKS_TAG),
-+ blockEntitiesSerialized,
-+ world.getGameTime()
-+ );
- }
-+ // Paper end
-
- private static void logErrors(ChunkPos chunkPos, int y, String message) {
- ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message);
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- // CraftBukkit end
-
- public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
-+ // Paper start
-+ return saveChunk(world, chunk, null);
-+ }
-+ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, @org.checkerframework.checker.nullness.qual.Nullable AsyncSaveData asyncsavedata) {
-+ // Paper end
- ChunkPos chunkcoordintpair = chunk.getPos();
- CompoundTag nbttagcompound = new CompoundTag();
-
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- nbttagcompound.putInt("xPos", chunkcoordintpair.x);
- nbttagcompound.putInt("yPos", chunk.getMinSection());
- nbttagcompound.putInt("zPos", chunkcoordintpair.z);
-- nbttagcompound.putLong("LastUpdate", world.getGameTime());
-+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading
- nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime());
- nbttagcompound.putString("Status", chunk.getStatus().getName());
- BlendingData blendingdata = chunk.getBlendingData();
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
- int j = chunk.getSectionIndexFromSectionY(i);
- boolean flag1 = j >= 0 && j < achunksection.length;
-- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-+ // Paper start - async chunk save for unload
-+ DataLayer nibblearray; // block light
-+ DataLayer nibblearray1; // sky light
-+ if (asyncsavedata == null) {
-+ nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData)
-+ nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData)
-+ } else {
-+ nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()];
-+ nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()];
-+ }
-+ // Paper end
-
- if (flag1 || nibblearray != null || nibblearray1 != null) {
- CompoundTag nbttagcompound1 = new CompoundTag();
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- nbttagcompound.putBoolean("isLightOn", true);
- }
-
-- ListTag nbttaglist1 = new ListTag();
-- Iterator iterator = chunk.getBlockEntitiesPos().iterator();
-+ // Paper start
-+ ListTag nbttaglist1;
-+ Iterator iterator;
-+ if (asyncsavedata != null) {
-+ nbttaglist1 = asyncsavedata.blockEntities;
-+ iterator = java.util.Collections.emptyIterator();
-+ } else {
-+ nbttaglist1 = new ListTag();
-+ iterator = chunk.getBlockEntitiesPos().iterator();
-+ }
-+ // Paper end
-
- CompoundTag nbttagcompound2;
-
-@@ -0,0 +0,0 @@ public class ChunkSerializer {
- nbttagcompound.put("CarvingMasks", nbttagcompound2);
- }
-
-+ // Paper start
-+ if (asyncsavedata != null) {
-+ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList);
-+ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList);
-+ } else {
- ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization());
-+ }
-+ // Paper end
- nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing()));
- CompoundTag nbttagcompound3 = new CompoundTag();
- Iterator iterator1 = chunk.getHeightmaps().iterator();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
- public class ChunkStorage implements AutoCloseable {
-
- public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
-- private final IOWorker worker;
-+ // Paper - nuke IO worker
- protected final DataFixer fixerUpper;
- @Nullable
- private volatile LegacyStructureDataHandler legacyStructureHandler;
-+ // Paper start - async chunk loading
-+ private final Object persistentDataLock = new Object(); // Paper
-+ public final RegionFileStorage regionFileCache;
-+ // Paper end - async chunk loading
-
- public ChunkStorage(Path directory, DataFixer dataFixer, boolean dsync) {
- this.fixerUpper = dataFixer;
-- this.worker = new IOWorker(directory, dsync, "chunk");
-+ // Paper start - async chunk io
-+ // remove IO worker
-+ this.regionFileCache = new RegionFileStorage(directory, dsync); // Paper - nuke IOWorker
-+ // Paper end - async chunk io
- }
-
- public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
-- return this.worker.isOldChunkAround(chunkPos, checkRadius);
-+ return true; // Paper - (for now, old unoptimised behavior) TODO implement later? the chunk status that blender uses SHOULD already have this radius loaded, no need to go back for it...
- }
-
- // CraftBukkit start
- private boolean check(ServerChunkCache cps, int x, int z) {
- ChunkPos pos = new ChunkPos(x, z);
- if (cps != null) {
-- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
-- if (cps.hasChunk(x, z)) {
-+ //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe
-+ if (cps.getChunkAtIfCachedImmediately(x, z) != null) { // Paper - isLoaded is a ticket level check, not a chunk loaded check!
- return true;
- }
- }
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
-
- public CompoundTag upgradeChunkTag(ResourceKey resourcekey, Supplier supplier, CompoundTag nbttagcompound, Optional>> optional, ChunkPos pos, @Nullable LevelAccessor generatoraccess) {
- // CraftBukkit end
-+ nbttagcompound = nbttagcompound.copy(); // Paper - defensive copy, another thread might modify this
- int i = ChunkStorage.getVersion(nbttagcompound);
-
- // CraftBukkit start
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
- if (i < 1493) {
- nbttagcompound = NbtUtils.update(this.fixerUpper, DataFixTypes.CHUNK, nbttagcompound, i, 1493);
- if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
-+ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
- LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
-
- nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
-+ } // Paper - Async chunk loading
- }
- }
-
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
- LegacyStructureDataHandler persistentstructurelegacy = this.legacyStructureHandler;
-
- if (persistentstructurelegacy == null) {
-- synchronized (this) {
-+ synchronized (this.persistentDataLock) { // Paper - async chunk loading
- persistentstructurelegacy = this.legacyStructureHandler;
- if (persistentstructurelegacy == null) {
- this.legacyStructureHandler = persistentstructurelegacy = LegacyStructureDataHandler.getLegacyStructureHandler(resourcekey, (DimensionDataStorage) supplier.get());
-@@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable {
- }
-
- public CompletableFuture> read(ChunkPos chunkPos) {
-- return this.worker.loadAsync(chunkPos);
-+ // Paper start - async chunk io
-+ try {
-+ return CompletableFuture.completedFuture(Optional.ofNullable(this.readSync(chunkPos)));
-+ } catch (Throwable thr) {
-+ return CompletableFuture.failedFuture(thr);
-+ }
-+ }
-+ @Nullable
-+ public CompoundTag readSync(ChunkPos chunkPos) throws IOException {
-+ return this.regionFileCache.read(chunkPos);
- }
-+ // Paper end - async chunk io
-
-- public void write(ChunkPos chunkPos, CompoundTag nbt) {
-- this.worker.store(chunkPos, nbt);
-+ // Paper start - async chunk io
-+ public void write(ChunkPos chunkPos, CompoundTag nbt) throws IOException {
-+ this.regionFileCache.write(chunkPos, nbt);
-+ // Paper end - Async chunk loading
- if (this.legacyStructureHandler != null) {
-+ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
- this.legacyStructureHandler.removeIndex(chunkPos.toLong());
-+ } // Paper - Async chunk loading
- }
-
- }
-
- public void flushWorker() {
-- this.worker.synchronize(true).join();
-+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.flush(); // Paper - nuke IO worker
- }
-
- public void close() throws IOException {
-- this.worker.close();
-+ this.regionFileCache.close(); // Paper - nuke IO worker
- }
-
- public ChunkScanAccess chunkScanner() {
-- return this.worker;
-+ // Paper start - nuke IO worker
-+ return ((chunkPos, streamTagVisitor) -> {
-+ try {
-+ this.regionFileCache.scanChunk(chunkPos, streamTagVisitor);
-+ return java.util.concurrent.CompletableFuture.completedFuture(null);
-+ } catch (IOException e) {
-+ throw new RuntimeException(e);
-+ }
-+ });
-+ // Paper end
- }
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- private final IntBuffer timestamps;
- @VisibleForTesting
- protected final RegionBitmap usedSectors;
-+ public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
-
- public RegionFile(Path file, Path directory, boolean dsync) throws IOException {
- this(file, directory, RegionFileVersion.VERSION_DEFLATE, dsync);
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- return (byteCount + 4096 - 1) / 4096;
- }
-
-- public boolean doesChunkExist(ChunkPos pos) {
-+ public synchronized boolean doesChunkExist(ChunkPos pos) { // Paper - synchronized
- int i = this.getOffset(pos);
-
- if (i == 0) {
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- }
-
- public void close() throws IOException {
-+ // Paper start - Prevent regionfiles from being closed during use
-+ this.fileLock.lock();
-+ synchronized (this) {
-+ try {
-+ // Paper end
- try {
- this.padToFullSector();
- } finally {
-@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
- this.file.close();
- }
- }
-+ } finally { // Paper start - Prevent regionfiles from being closed during use
-+ this.fileLock.unlock();
-+ }
-+ } // Paper end
-
- }
-
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- this.sync = dsync;
- }
-
-- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
-+ // Paper start
-+ public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) {
-+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()));
-+ }
-+
-+ public synchronized boolean chunkExists(ChunkPos pos) throws IOException {
-+ RegionFile regionfile = getRegionFile(pos, true);
-+
-+ return regionfile != null ? regionfile.hasChunk(pos) : false;
-+ }
-+
-+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
-+ return this.getRegionFile(chunkcoordintpair, existingOnly, false);
-+ }
-+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException {
-+ // Paper end
- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
-
- if (regionfile != null) {
-+ // Paper start
-+ if (lock) {
-+ // must be in this synchronized block
-+ regionfile.fileLock.lock();
-+ }
-+ // Paper end
- return regionfile;
- } else {
- if (this.regionCache.size() >= 256) {
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- RegionFile regionfile1 = new RegionFile(path1, this.folder, this.sync);
-
- this.regionCache.putAndMoveToFirst(i, regionfile1);
-+ // Paper start
-+ if (lock) {
-+ // must be in this synchronized block
-+ regionfile1.fileLock.lock();
-+ }
-+ // Paper end
- return regionfile1;
- }
- }
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- @Nullable
- public CompoundTag read(ChunkPos pos) throws IOException {
- // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
-- RegionFile regionfile = this.getRegionFile(pos, true);
-+ RegionFile regionfile = this.getRegionFile(pos, true, true); // Paper
- if (regionfile == null) {
- return null;
- }
- // CraftBukkit end
-+ try { // Paper
- DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
-
- CompoundTag nbttagcompound;
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- }
-
- return nbttagcompound;
-+ } finally { // Paper start
-+ regionfile.fileLock.unlock();
-+ } // Paper end
- }
-
- public void scanChunk(ChunkPos chunkcoordintpair, StreamTagVisitor streamtagvisitor) throws IOException {
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- }
-
- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
-- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
-+ RegionFile regionfile = this.getRegionFile(pos, false, true); // CraftBukkit // Paper
-+ try { // Paper
-
- if (nbt == null) {
- regionfile.clear(pos);
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- }
- }
-
-+ } finally { // Paper start
-+ regionfile.fileLock.unlock();
-+ } // Paper end
- }
-
-- public void close() throws IOException {
-+ public synchronized void close() throws IOException { // Paper -> synchronized
- ExceptionCollector exceptionsuppressor = new ExceptionCollector<>();
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
-@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- exceptionsuppressor.throwIfPresent();
- }
-
-- public void flush() throws IOException {
-+ public synchronized void flush() throws IOException { // Paper - synchronize
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.LevelHeightAccessor;
- import org.slf4j.Logger;
-
--public class SectionStorage implements AutoCloseable {
-+public class SectionStorage extends RegionFileStorage implements AutoCloseable { // Paper - nuke IOWorker
- private static final Logger LOGGER = LogUtils.getLogger();
- private static final String SECTIONS_TAG = "Sections";
-- private final IOWorker worker;
-+ // Paper - remove mojang I/O thread
- private final Long2ObjectMap> storage = new Long2ObjectOpenHashMap<>();
- public final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet();
- private final Function> codec;
-@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable {
- protected final LevelHeightAccessor levelHeightAccessor;
-
- public SectionStorage(Path path, Function> codecFactory, Function factory, DataFixer dataFixer, DataFixTypes dataFixTypes, boolean dsync, RegistryAccess dynamicRegistryManager, LevelHeightAccessor world) {
-+ super(path, dsync); // Paper - remove mojang I/O thread
- this.codec = codecFactory;
- this.factory = factory;
- this.fixerUpper = dataFixer;
- this.type = dataFixTypes;
- this.registryAccess = dynamicRegistryManager;
- this.levelHeightAccessor = world;
-- this.worker = new IOWorker(path, dsync, path.getFileName().toString());
-+ // Paper - remove mojang I/O thread
- }
-
- protected void tick(BooleanSupplier shouldKeepTicking) {
-@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable {
- }
-
- private CompletableFuture> tryRead(ChunkPos pos) {
-- return this.worker.loadAsync(pos).exceptionally((throwable) -> {
-- if (throwable instanceof IOException iOException) {
-- LOGGER.error("Error reading chunk {} data from disk", pos, iOException);
-- return Optional.empty();
-- } else {
-- throw new CompletionException(throwable);
-- }
-- });
-+ // Paper start - async chunk io
-+ try {
-+ return CompletableFuture.completedFuture(Optional.ofNullable(this.read(pos)));
-+ } catch (Throwable thr) {
-+ return CompletableFuture.failedFuture(thr);
-+ }
-+ // Paper end - async chunk io
-+ }
-+
-+ // Paper start - async chunk io
-+ public void loadInData(ChunkPos chunkPos, CompoundTag compound) {
-+ this.readColumn(chunkPos, RegistryOps.create(NbtOps.INSTANCE, this.registryAccess), compound);
- }
-+ // Paper end - aync chnnk i
-
- private void readColumn(ChunkPos pos, DynamicOps ops, @Nullable T data) {
- if (data == null) {
-@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable {
- Dynamic dynamic = this.writeColumn(pos, registryOps);
- Tag tag = dynamic.getValue();
- if (tag instanceof CompoundTag) {
-- this.worker.store(pos, (CompoundTag)tag);
-+ try { this.write(pos, (CompoundTag)tag); } catch (IOException ioexception) { SectionStorage.LOGGER.error("Error writing data to disk", ioexception); } // Paper - nuke IOWorker
- } else {
- LOGGER.error("Expected compound tag, got {}", (Object)tag);
- }
-@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable {
- return new Dynamic<>(ops, ops.createMap(ImmutableMap.of(ops.createString("Sections"), ops.createMap(map), ops.createString("DataVersion"), ops.createInt(SharedConstants.getCurrentVersion().getWorldVersion()))));
- }
-
-+ // Paper start - internal get data function, copied from above
-+ private CompoundTag getDataInternal(ChunkPos pos) {
-+ RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, this.registryAccess);
-+ Dynamic dynamic = this.writeColumn(pos, registryOps);
-+ Tag nbtbase = (Tag) dynamic.getValue();
-+
-+ if (nbtbase instanceof CompoundTag) {
-+ return (CompoundTag)nbtbase;
-+ } else {
-+ SectionStorage.LOGGER.error("Expected compound tag, got {}", nbtbase);
-+ }
-+ return null;
-+ }
-+ // Paper end
-+
- private static long getKey(ChunkPos chunkPos, int y) {
- return SectionPos.asLong(chunkPos.x, y, chunkPos.z);
- }
-@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable {
-
- @Override
- public void close() throws IOException {
-- this.worker.close();
-+ //this.worker.close(); // Paper - nuke I/O worker - don't call the worker
-+ super.close(); // Paper - nuke I/O worker - call super.close method which is responsible for closing used files.
-+ }
-+
-+ // Paper start - get data function
-+ public CompoundTag getData(ChunkPos chunkcoordintpair) {
-+ // Note: Copied from above
-+ // This is checking if the data needs to be written, then it builds it later in getDataInternal(ChunkCoordIntPair)
-+ if (!this.dirty.isEmpty()) {
-+ for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); ++i) {
-+ long j = SectionPos.of(chunkcoordintpair, i).asLong();
-+
-+ if (this.dirty.contains(j)) {
-+ return this.getDataInternal(chunkcoordintpair);
-+ }
-+ }
-+ }
-+ return null;
- }
-+ // Paper end
- }
-diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/WatchdogThread.java
-+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
- //
- log.log( Level.SEVERE, "------------------------------" );
- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" );
-+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
- WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
- log.log( Level.SEVERE, "------------------------------" );
- //
diff --git a/patches/server/Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/server/Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
index f691394c5a..8421325bc2 100644
--- a/patches/server/Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
+++ b/patches/server/Attempt-to-recalculate-regionfile-header-if-it-is-co.patch
@@ -694,7 +694,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private final boolean isChunkData; // Paper
+
- RegionFileStorage(Path directory, boolean dsync) {
+ protected RegionFileStorage(Path directory, boolean dsync) { // Paper - protected constructor
+ // Paper start - add isChunkData param
+ this(directory, dsync, false);
+ }
diff --git a/patches/server/Avoid-hopper-searches-if-there-are-no-items.patch b/patches/server/Avoid-hopper-searches-if-there-are-no-items.patch
deleted file mode 100644
index 3e24d9413a..0000000000
--- a/patches/server/Avoid-hopper-searches-if-there-are-no-items.patch
+++ /dev/null
@@ -1,124 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: CullanP
-Date: Thu, 3 Mar 2016 02:13:38 -0600
-Subject: [PATCH] Avoid hopper searches if there are no items
-
-Hoppers searching for items and minecarts is the most expensive part of hopper ticking.
-We keep track of the number of minecarts and items in a chunk.
-If there are no items in the chunk, we skip searching for items.
-If there are no minecarts in the chunk, we skip searching for them.
-
-Usually hoppers aren't near items, so we can skip most item searches.
-And since minecart hoppers are used _very_ rarely near we can avoid alot of searching there.
-
-Combined, this adds up a lot.
-
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- }
- }
-
-- });
-+ }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper
- return list;
- }
-
-diff --git a/src/main/java/net/minecraft/world/level/entity/EntitySection.java b/src/main/java/net/minecraft/world/level/entity/EntitySection.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/EntitySection.java
-+++ b/src/main/java/net/minecraft/world/level/entity/EntitySection.java
-@@ -0,0 +0,0 @@ public class EntitySection {
- private static final Logger LOGGER = LogUtils.getLogger();
- private final ClassInstanceMultiMap storage;
- private Visibility chunkStatus;
-+ // Paper start - track number of items and minecarts
-+ public int itemCount;
-+ public int inventoryEntityCount;
-+ // Paper end
-
- public EntitySection(Class entityClass, Visibility status) {
- this.chunkStatus = status;
-@@ -0,0 +0,0 @@ public class EntitySection {
- }
-
- public void add(T entity) {
-+ // Paper start
-+ if (entity instanceof net.minecraft.world.entity.item.ItemEntity) {
-+ this.itemCount++;
-+ } else if (entity instanceof net.minecraft.world.Container) {
-+ this.inventoryEntityCount++;
-+ }
-+ // Paper end
- this.storage.add(entity);
- }
-
- public boolean remove(T entity) {
-+ // Paper start
-+ if (entity instanceof net.minecraft.world.entity.item.ItemEntity) {
-+ this.itemCount--;
-+ } else if (entity instanceof net.minecraft.world.Container) {
-+ this.inventoryEntityCount--;
-+ }
-+ // Paper end
- return this.storage.remove(entity);
- }
-
-diff --git a/src/main/java/net/minecraft/world/level/entity/EntitySectionStorage.java b/src/main/java/net/minecraft/world/level/entity/EntitySectionStorage.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/EntitySectionStorage.java
-+++ b/src/main/java/net/minecraft/world/level/entity/EntitySectionStorage.java
-@@ -0,0 +0,0 @@ public class EntitySectionStorage {
- }
-
- public void getEntities(AABB box, Consumer action) {
-+ // Paper start
-+ this.getEntities(box, action, false);
-+ }
-+ public void getEntities(AABB box, Consumer action, boolean isContainerSearch) {
-+ // Paper end
- this.forEachAccessibleNonEmptySection(box, (section) -> {
-+ if (isContainerSearch && section.inventoryEntityCount <= 0) return; // Paper
- section.getEntities(box, action);
- });
- }
-
- public void getEntities(EntityTypeTest filter, AABB box, Consumer action) {
- this.forEachAccessibleNonEmptySection(box, (section) -> {
-+ if (filter.getBaseClass() == net.minecraft.world.entity.item.ItemEntity.class && section.itemCount <= 0) return; // Paper
- section.getEntities(filter, box, action);
- });
- }
-diff --git a/src/main/java/net/minecraft/world/level/entity/LevelEntityGetter.java b/src/main/java/net/minecraft/world/level/entity/LevelEntityGetter.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/LevelEntityGetter.java
-+++ b/src/main/java/net/minecraft/world/level/entity/LevelEntityGetter.java
-@@ -0,0 +0,0 @@ public interface LevelEntityGetter {
- void get(EntityTypeTest filter, Consumer action);
-
- void get(AABB box, Consumer action);
-+ void get(AABB box, Consumer action, boolean isContainerSearch); // Paper
-
- void get(EntityTypeTest filter, AABB box, Consumer action);
- }
-diff --git a/src/main/java/net/minecraft/world/level/entity/LevelEntityGetterAdapter.java b/src/main/java/net/minecraft/world/level/entity/LevelEntityGetterAdapter.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/LevelEntityGetterAdapter.java
-+++ b/src/main/java/net/minecraft/world/level/entity/LevelEntityGetterAdapter.java
-@@ -0,0 +0,0 @@ public class LevelEntityGetterAdapter implements LevelEn
-
- @Override
- public void get(AABB box, Consumer action) {
-- this.sectionStorage.getEntities(box, action);
-+ // Paper start
-+ this.get(box, action, false);
-+ }
-+ @Override
-+ public void get(AABB box, Consumer action, boolean isContainerSearch) {
-+ this.sectionStorage.getEntities(box, action, isContainerSearch);
-+ // Paper end
- }
-
- @Override
diff --git a/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch b/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
index 3d91df7fa6..a623c58ad2 100644
--- a/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
+++ b/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
@@ -16,8 +16,8 @@ diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -0,0 +0,0 @@ import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
- import java.lang.ref.Cleaner;
+@@ -0,0 +0,0 @@ import java.lang.ref.Cleaner;
+ import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
+import net.minecraft.nbt.CompoundTag;
diff --git a/patches/server/Chunk-Save-Reattempt.patch b/patches/server/Chunk-Save-Reattempt.patch
index 708cf94dc6..6d260baab6 100644
--- a/patches/server/Chunk-Save-Reattempt.patch
+++ b/patches/server/Chunk-Save-Reattempt.patch
@@ -23,8 +23,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
- RegionFile regionfile = this.getRegionFile(pos, false, true); // CraftBukkit // Paper
+ }
+ // Paper end - rewrite chunk system
try { // Paper
+ int attempts = 0; Exception laste = null; while (attempts++ < 5) { try { // Paper
diff --git a/patches/server/Chunk-debug-command.patch b/patches/server/Chunk-debug-command.patch
deleted file mode 100644
index 8986463d4c..0000000000
--- a/patches/server/Chunk-debug-command.patch
+++ /dev/null
@@ -1,432 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sat, 1 Jun 2019 13:00:55 -0700
-Subject: [PATCH] Chunk debug command
-
-Prints all chunk information to a text file into the debug
-folder in the root server folder. The format is in JSON, and
-the data format is described in MCUtil#dumpChunks(File)
-
-The command will output server version and all online players to the
-file as well. We do not log anything but the location, world and
-username of the player.
-
-Also logs the value of these config values (note not all are paper's):
-- keep spawn loaded value
-- spawn radius
-- view distance
-
-Each chunk has the following logged:
-- Coordinate
-- Ticket level & its corresponding state
-- Whether it is queued for unload
-- Chunk status (may be unloaded)
-- All tickets on the chunk
-
-Example log:
-https://gist.githubusercontent.com/Spottedleaf/0131e7710ffd5d531e5fd246c3367380/raw/169ae1b2e240485f99bc7a6bd8e78d90e3af7397/chunks-2019-06-01_19.57.05.txt
-
-For references on certain keywords (ticket, status, etc), please see:
-
-https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528273&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528273
-https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528577&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528577
-
-diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/command/PaperCommand.java
-+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
-@@ -0,0 +0,0 @@
- package io.papermc.paper.command;
-
-+import io.papermc.paper.command.subcommands.ChunkDebugCommand;
- import io.papermc.paper.command.subcommands.EntityCommand;
- import io.papermc.paper.command.subcommands.HeapDumpCommand;
- import io.papermc.paper.command.subcommands.ReloadCommand;
-@@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
- commands.put(Set.of("entity"), new EntityCommand());
- commands.put(Set.of("reload"), new ReloadCommand());
- commands.put(Set.of("version"), new VersionCommand());
-+ commands.put(Set.of("debug", "chunkinfo"), new ChunkDebugCommand());
-
- return commands.entrySet().stream()
- .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import io.papermc.paper.command.CommandUtil;
-+import io.papermc.paper.command.PaperSubcommand;
-+import java.io.File;
-+import java.time.LocalDateTime;
-+import java.time.format.DateTimeFormatter;
-+import java.util.ArrayList;
-+import java.util.Collections;
-+import java.util.List;
-+import java.util.Locale;
-+import net.minecraft.server.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ServerLevel;
-+import org.bukkit.Bukkit;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class ChunkDebugCommand implements PaperSubcommand {
-+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> this.doDebug(sender, args);
-+ case "chunkinfo" -> this.doChunkInfo(sender, args);
-+ }
-+ return true;
-+ }
-+
-+ @Override
-+ public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> {
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
-+ }
-+ }
-+ case "chunkinfo" -> {
-+ List worldNames = new ArrayList<>();
-+ worldNames.add("*");
-+ for (org.bukkit.World world : Bukkit.getWorlds()) {
-+ worldNames.add(world.getName());
-+ }
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
-+ }
-+ }
-+ }
-+ return Collections.emptyList();
-+ }
-+
-+ private void doChunkInfo(final CommandSender sender, final String[] args) {
-+ List worlds;
-+ if (args.length < 1 || args[0].equals("*")) {
-+ worlds = Bukkit.getWorlds();
-+ } else {
-+ worlds = new ArrayList<>(args.length);
-+ for (final String arg : args) {
-+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
-+ if (world == null) {
-+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
-+ return;
-+ }
-+ worlds.add(world);
-+ }
-+ }
-+
-+ int accumulatedTotal = 0;
-+ int accumulatedInactive = 0;
-+ int accumulatedBorder = 0;
-+ int accumulatedTicking = 0;
-+ int accumulatedEntityTicking = 0;
-+
-+ for (final org.bukkit.World bukkitWorld : worlds) {
-+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
-+
-+ int total = 0;
-+ int inactive = 0;
-+ int border = 0;
-+ int ticking = 0;
-+ int entityTicking = 0;
-+
-+ for (final ChunkHolder chunk : net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world)) {
-+ if (chunk.getFullChunkNowUnchecked() == null) {
-+ continue;
-+ }
-+
-+ ++total;
-+
-+ ChunkHolder.FullChunkStatus state = chunk.getFullStatus();
-+
-+ switch (state) {
-+ case INACCESSIBLE -> ++inactive;
-+ case BORDER -> ++border;
-+ case TICKING -> ++ticking;
-+ case ENTITY_TICKING -> ++entityTicking;
-+ }
-+ }
-+
-+ accumulatedTotal += total;
-+ accumulatedInactive += inactive;
-+ accumulatedBorder += border;
-+ accumulatedTicking += ticking;
-+ accumulatedEntityTicking += entityTicking;
-+
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(total),
-+ text(" Inactive: ", BLUE), text(inactive),
-+ text(" Border: ", BLUE), text(border),
-+ text(" Ticking: ", BLUE), text(ticking),
-+ text(" Entity: ", BLUE), text(entityTicking)
-+ ));
-+ }
-+ if (worlds.size() > 1) {
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(accumulatedTotal),
-+ text(" Inactive: ", BLUE), text(accumulatedInactive),
-+ text(" Border: ", BLUE), text(accumulatedBorder),
-+ text(" Ticking: ", BLUE), text(accumulatedTicking),
-+ text(" Entity: ", BLUE), text(accumulatedEntityTicking)
-+ ));
-+ }
-+ }
-+
-+ private void doDebug(final CommandSender sender, final String[] args) {
-+ if (args.length < 1) {
-+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
-+ return;
-+ }
-+
-+ final String debugType = args[0].toLowerCase(Locale.ENGLISH);
-+ switch (debugType) {
-+ case "chunks" -> {
-+ if (args.length >= 2 && args[1].toLowerCase(Locale.ENGLISH).equals("help")) {
-+ sender.sendMessage(text("Use /paper debug chunks [world] to dump loaded chunk information to a file", RED));
-+ break;
-+ }
-+ File file = new File(new File(new File("."), "debug"),
-+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
-+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN));
-+ try {
-+ MCUtil.dumpChunks(file);
-+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
-+ } catch (Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
-+ }
-+ }
-+ // "help" & default
-+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
-+ }
-+ }
-+
-+}
-diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MCUtil.java
-+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -0,0 +0,0 @@
- package net.minecraft.server;
-
- import com.google.common.util.concurrent.ThreadFactoryBuilder;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonObject;
-+import com.google.gson.internal.Streams;
-+import com.google.gson.stream.JsonWriter;
-+import com.mojang.datafixers.util.Either;
- import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
- import java.lang.ref.Cleaner;
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.DistanceManager;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.Ticket;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.ClipContext;
- import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ChunkStatus;
- import org.apache.commons.lang.exception.ExceptionUtils;
- import org.bukkit.Location;
- import org.bukkit.block.BlockFace;
-@@ -0,0 +0,0 @@ import org.spigotmc.AsyncCatcher;
-
- import javax.annotation.Nonnull;
- import javax.annotation.Nullable;
-+import java.io.*;
-+import java.nio.charset.StandardCharsets;
- import java.util.List;
- import java.util.Queue;
-+import java.util.Set;
- import java.util.concurrent.CompletableFuture;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.LinkedBlockingQueue;
-@@ -0,0 +0,0 @@ public final class MCUtil {
- }
- }
-
-+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
-+ return chunk.getChunkHolderStatus();
-+ }
-+
-+ public static void dumpChunks(File file) throws IOException {
-+ file.getParentFile().mkdirs();
-+ file.createNewFile();
-+ /*
-+ * Json format:
-+ *
-+ * Main data format:
-+ * -server-version:
-+ * -data-version:
-+ * -worlds:
-+ * -name:
-+ * -view-distance:
-+ * -keep-spawn-loaded:
-+ * -keep-spawn-loaded-range:
-+ * -visible-chunk-count:
-+ * -loaded-chunk-count:
-+ * -verified-fully-loaded-chunks:
-+ * -players:
-+ * -chunk-data:
-+ *
-+ * Player format:
-+ * -name:
-+ * -x:
-+ * -y:
-+ * -z:
-+ *
-+ * Chunk Format:
-+ * -x:
-+ * -z:
-+ * -ticket-level:
-+ * -state:
-+ * -queued-for-unload:
-+ * -status:
-+ * -tickets:
-+ *
-+ *
-+ * Ticket format:
-+ * -ticket-type:
-+ * -ticket-level:
-+ * -add-tick:
-+ * -object-reason: // This depends on the type of ticket. ie POST_TELEPORT -> entity id
-+ */
-+ List worlds = org.bukkit.Bukkit.getWorlds();
-+ JsonObject data = new JsonObject();
-+
-+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion());
-+ data.addProperty("data-version", 0);
-+
-+ JsonArray worldsData = new JsonArray();
-+
-+ for (org.bukkit.World bukkitWorld : worlds) {
-+ JsonObject worldData = new JsonObject();
-+
-+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
-+ ChunkMap chunkMap = world.getChunkSource().chunkMap;
-+ DistanceManager chunkMapDistance = chunkMap.distanceManager;
-+ List allChunks = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world);
-+ List players = world.players;
-+
-+ int fullLoadedChunks = 0;
-+
-+ for (ChunkHolder chunk : allChunks) {
-+ if (chunk.getFullChunkNowUnchecked() != null) {
-+ ++fullLoadedChunks;
-+ }
-+ }
-+
-+ // sorting by coordinate makes the log easier to read
-+ allChunks.sort((ChunkHolder v1, ChunkHolder v2) -> {
-+ if (v1.pos.x != v2.pos.x) {
-+ return Integer.compare(v1.pos.x, v2.pos.x);
-+ }
-+ return Integer.compare(v1.pos.z, v2.pos.z);
-+ });
-+
-+ worldData.addProperty("name", world.getWorld().getName());
-+ worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
-+ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
-+ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16);
-+ worldData.addProperty("visible-chunk-count", allChunks.size());
-+ worldData.addProperty("loaded-chunk-count", chunkMap.entitiesInLevel.size());
-+ worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks);
-+
-+ JsonArray playersData = new JsonArray();
-+
-+ for (ServerPlayer player : players) {
-+ JsonObject playerData = new JsonObject();
-+
-+ playerData.addProperty("name", player.getScoreboardName());
-+ playerData.addProperty("x", player.getX());
-+ playerData.addProperty("y", player.getY());
-+ playerData.addProperty("z", player.getZ());
-+
-+ playersData.add(playerData);
-+
-+ }
-+
-+ worldData.add("players", playersData);
-+
-+ JsonArray chunksData = new JsonArray();
-+
-+ for (ChunkHolder playerChunk : allChunks) {
-+ JsonObject chunkData = new JsonObject();
-+
-+ Set> tickets = chunkMapDistance.tickets.get(playerChunk.pos.longKey);
-+ ChunkStatus status = getChunkStatus(playerChunk);
-+
-+ chunkData.addProperty("x", playerChunk.pos.x);
-+ chunkData.addProperty("z", playerChunk.pos.z);
-+ chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
-+ chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString());
-+ chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey));
-+ chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
-+
-+ JsonArray ticketsData = new JsonArray();
-+
-+ if (tickets != null) {
-+ for (Ticket> ticket : tickets) {
-+ JsonObject ticketData = new JsonObject();
-+
-+ ticketData.addProperty("ticket-type", ticket.getType().toString());
-+ ticketData.addProperty("ticket-level", ticket.getTicketLevel());
-+ ticketData.addProperty("object-reason", String.valueOf(ticket.key));
-+ ticketData.addProperty("add-tick", ticket.createdTick);
-+
-+ ticketsData.add(ticketData);
-+ }
-+ }
-+
-+ chunkData.add("tickets", ticketsData);
-+ chunksData.add(chunkData);
-+ }
-+
-+
-+ worldData.add("chunk-data", chunksData);
-+ worldsData.add(worldData);
-+ }
-+
-+ data.add("worlds", worldsData);
-+
-+ StringWriter stringWriter = new StringWriter();
-+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
-+ jsonWriter.setIndent(" ");
-+ jsonWriter.setLenient(false);
-+ Streams.write(data, jsonWriter);
-+
-+ String fileData = stringWriter.toString();
-+
-+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
-+ out.print(fileData);
-+ }
-+ }
-+
- public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
- return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
- }
diff --git a/patches/server/ChunkMapDistance-CME.patch b/patches/server/ChunkMapDistance-CME.patch
deleted file mode 100644
index 64770b9915..0000000000
--- a/patches/server/ChunkMapDistance-CME.patch
+++ /dev/null
@@ -1,84 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Shane Freeder
-Date: Wed, 29 May 2019 04:01:22 +0100
-Subject: [PATCH] ChunkMapDistance CME
-
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -0,0 +0,0 @@ public class ChunkHolder {
- private boolean resendLight;
- private CompletableFuture pendingFullStateConfirmation;
-
-+ boolean isUpdateQueued = false; // Paper
- private final ChunkMap chunkMap; // Paper
-
- // Paper start
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
- private final TickingTracker tickingTicketsTracker = new TickingTracker();
- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33);
-- final Set chunksToUpdateFutures = Sets.newHashSet();
-+ // Paper start use a queue, but still keep unique requirement
-+ public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() {
-+ @Override
-+ public boolean add(ChunkHolder o) {
-+ if (o.isUpdateQueued) return true;
-+ o.isUpdateQueued = true;
-+ return super.add(o);
-+ }
-+ };
-+ // Paper end
- final ChunkTaskPriorityQueueSorter ticketThrottler;
- final ProcessorHandle> ticketThrottlerInput;
- final ProcessorHandle ticketThrottlerReleaser;
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
- ;
- }
-
-- if (!this.chunksToUpdateFutures.isEmpty()) {
-- // CraftBukkit start
-- // Iterate pending chunk updates with protection against concurrent modification exceptions
-- java.util.Iterator iter = this.chunksToUpdateFutures.iterator();
-- int expectedSize = this.chunksToUpdateFutures.size();
-- do {
-- ChunkHolder playerchunk = iter.next();
-- iter.remove();
-- expectedSize--;
--
-- playerchunk.updateFutures(chunkStorage, this.mainThreadExecutor);
--
-- // Reset iterator if set was modified using add()
-- if (this.chunksToUpdateFutures.size() != expectedSize) {
-- expectedSize = this.chunksToUpdateFutures.size();
-- iter = this.chunksToUpdateFutures.iterator();
-- }
-- } while (iter.hasNext());
-- // CraftBukkit end
--
-+ // Paper start
-+ if (!this.pendingChunkUpdates.isEmpty()) {
-+ while(!this.pendingChunkUpdates.isEmpty()) {
-+ ChunkHolder remove = this.pendingChunkUpdates.remove();
-+ remove.isUpdateQueued = false;
-+ remove.updateFutures(chunkStorage, this.mainThreadExecutor);
-+ }
-+ // Paper end
- return true;
- } else {
- if (!this.ticketsToRelease.isEmpty()) {
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
- if (k != level) {
- playerchunk = DistanceManager.this.updateChunkScheduling(id, level, playerchunk, k);
- if (playerchunk != null) {
-- DistanceManager.this.chunksToUpdateFutures.add(playerchunk);
-+ DistanceManager.this.pendingChunkUpdates.add(playerchunk);
- }
-
- }
diff --git a/patches/server/ConcurrentUtil.patch b/patches/server/ConcurrentUtil.patch
index 9d641d67ba..d5b8e6d745 100644
--- a/patches/server/ConcurrentUtil.patch
+++ b/patches/server/ConcurrentUtil.patch
@@ -1412,6 +1412,160 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ }
+}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.collection;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import java.lang.invoke.VarHandle;
++import java.util.ConcurrentModificationException;
++
++/**
++ * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics,
++ * and the writer side of the queue is ordered by release semantics.
++ */
++// TODO test
++public class SRSWLinkedQueue {
++
++ // always non-null
++ protected LinkedNode head;
++
++ // always non-null
++ protected LinkedNode tail;
++
++ /* IMPL NOTE: Leave hashCode and equals to their defaults */
++
++ public SRSWLinkedQueue() {
++ final LinkedNode dummy = new LinkedNode<>(null, null);
++ this.head = this.tail = dummy;
++ }
++
++ /**
++ * Must be the reader thread.
++ *
++ *
++ * Returns, without removing, the first element of this queue.
++ *
++ * @return Returns, without removing, the first element of this queue.
++ */
++ public E peekFirst() {
++ LinkedNode head = this.head;
++ E ret = head.getElementPlain();
++ if (ret == null) {
++ head = head.getNextAcquire();
++ if (head == null) {
++ // empty
++ return null;
++ }
++ // update head reference for next poll() call
++ this.head = head;
++ // guaranteed to be non-null
++ ret = head.getElementPlain();
++ if (ret == null) {
++ throw new ConcurrentModificationException("Multiple reader threads");
++ }
++ }
++
++ return ret;
++ }
++
++ /**
++ * Must be the reader thread.
++ *
++ *
++ * Returns and removes the first element of this queue.
++ *
++ * @return Returns and removes the first element of this queue.
++ */
++ public E poll() {
++ LinkedNode head = this.head;
++ E ret = head.getElementPlain();
++ if (ret == null) {
++ head = head.getNextAcquire();
++ if (head == null) {
++ // empty
++ return null;
++ }
++ // guaranteed to be non-null
++ ret = head.getElementPlain();
++ if (ret == null) {
++ throw new ConcurrentModificationException("Multiple reader threads");
++ }
++ }
++
++ head.setElementPlain(null);
++ LinkedNode next = head.getNextAcquire();
++ this.head = next == null ? head : next;
++
++ return ret;
++ }
++
++ /**
++ * Must be the writer thread.
++ *
++ *
++ * Adds the element to the end of the queue.
++ *
++ *
++ * @throws NullPointerException If the provided element is null
++ */
++ public void addLast(final E element) {
++ Validate.notNull(element, "Provided element cannot be null");
++ final LinkedNode append = new LinkedNode<>(element, null);
++
++ this.tail.setNextRelease(append);
++ this.tail = append;
++ }
++
++ protected static final class LinkedNode {
++
++ protected volatile Object element;
++ protected volatile LinkedNode next;
++
++ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
++
++ protected LinkedNode(final Object element, final LinkedNode next) {
++ ELEMENT_HANDLE.set(this, element);
++ NEXT_HANDLE.set(this, next);
++ }
++
++ /* element */
++
++ @SuppressWarnings("unchecked")
++ protected final E getElementPlain() {
++ return (E)ELEMENT_HANDLE.get(this);
++ }
++
++ protected final void setElementPlain(final E update) {
++ ELEMENT_HANDLE.set(this, (Object)update);
++ }
++ /* next */
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode getNextPlain() {
++ return (LinkedNode)NEXT_HANDLE.get(this);
++ }
++
++ @SuppressWarnings("unchecked")
++ protected final LinkedNode getNextAcquire() {
++ return (LinkedNode)NEXT_HANDLE.getAcquire(this);
++ }
++
++ protected final void setNextPlain(final LinkedNode next) {
++ NEXT_HANDLE.set(this, next);
++ }
++
++ protected final void setNextRelease(final LinkedNode next) {
++ NEXT_HANDLE.setRelease(this, next);
++ }
++ }
++}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -1575,11 +1729,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ * @throws IllegalStateException If the current thread is not allowed to wait
+ */
+ public default void waitUntilAllExecuted() throws IllegalStateException {
-+ long failures = 9L; // start out at 1ms
++ long failures = 1L; // start at 0.25ms
+
+ while (!this.haveAllTasksExecuted()) {
+ Thread.yield();
-+ failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 5_000_000L); // 500us, 5ms
++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
+ }
+ }
+
diff --git a/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
index f326b3dcdc..46ff12fb1a 100644
--- a/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
+++ b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Sat, 4 Apr 2020 17:00:20 -0700
Subject: [PATCH] Consolidate flush calls for entity tracker packets
diff --git a/patches/server/Correctly-handle-recursion-for-chunkholder-updates.patch b/patches/server/Correctly-handle-recursion-for-chunkholder-updates.patch
deleted file mode 100644
index 0b5bd3a9db..0000000000
--- a/patches/server/Correctly-handle-recursion-for-chunkholder-updates.patch
+++ /dev/null
@@ -1,37 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sun, 21 Mar 2021 17:32:47 -0700
-Subject: [PATCH] Correctly handle recursion for chunkholder updates
-
-If a chunk ticket level is brought up while unloading it would
-cause a recursive call which would handle the increase but then
-the caller would think the chunk would be unloaded.
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -0,0 +0,0 @@ public class ChunkHolder {
- playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
- }
-
-+ protected long updateCount; // Paper - correctly handle recursion
- protected void updateFutures(ChunkMap chunkStorage, Executor executor) {
- io.papermc.paper.util.TickThread.ensureTickThread("Async ticket level update"); // Paper
-+ long updateCount = ++this.updateCount; // Paper - correctly handle recursion
- ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel);
- ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel);
- boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE;
-@@ -0,0 +0,0 @@ public class ChunkHolder {
-
- // Run callback right away if the future was already done
- chunkStorage.callbackExecutor.run();
-+ // Paper start - correctly handle recursion
-+ if (this.updateCount != updateCount) {
-+ // something else updated ticket level for us.
-+ return;
-+ }
-+ // Paper end - correctly handle recursion
- }
- // CraftBukkit end
-
diff --git a/patches/server/Delay-Chunk-Unloads-based-on-Player-Movement.patch b/patches/server/Delay-Chunk-Unloads-based-on-Player-Movement.patch
deleted file mode 100644
index 3ee047cff4..0000000000
--- a/patches/server/Delay-Chunk-Unloads-based-on-Player-Movement.patch
+++ /dev/null
@@ -1,89 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar
-Date: Sat, 18 Jun 2016 23:22:12 -0400
-Subject: [PATCH] Delay Chunk Unloads based on Player Movement
-
-When players are moving in the world, doing things such as building or exploring,
-they will commonly go back and forth in a small area. This causes a ton of chunk load
-and unload activity on the edge chunks of their view distance.
-
-A simple back and forth movement in 6 blocks could spam a chunk to thrash a
-loading and unload cycle over and over again.
-
-This is very wasteful. This system introduces a delay of inactivity on a chunk
-before it actually unloads, which will be handled by the ticket expiry process.
-
-This allows servers with smaller worlds who do less long distance exploring to stop
-wasting cpu cycles on saving/unloading/reloading chunks repeatedly.
-
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
- boolean removed = false; // CraftBukkit
- if (arraysetsorted.remove(ticket)) {
- removed = true; // CraftBukkit
-+ // Paper start - delay chunk unloads for player tickets
-+ long delayChunkUnloadsBy = chunkMap.level.paperConfig().chunks.delayChunkUnloadsBy.ticks();
-+ if (ticket.getType() == TicketType.PLAYER && delayChunkUnloadsBy > 0) {
-+ boolean hasPlayer = false;
-+ for (Ticket> ticket1 : arraysetsorted) {
-+ if (ticket1.getType() == TicketType.PLAYER) {
-+ hasPlayer = true;
-+ break;
-+ }
-+ }
-+ ChunkHolder playerChunk = chunkMap.getUpdatingChunkIfPresent(i);
-+ if (!hasPlayer && playerChunk != null && playerChunk.isFullChunkReady()) {
-+ Ticket delayUnload = new Ticket(TicketType.DELAY_UNLOAD, 33, i);
-+ delayUnload.delayUnloadBy = delayChunkUnloadsBy;
-+ delayUnload.setCreatedTick(this.ticketTickCounter);
-+ arraysetsorted.remove(delayUnload);
-+ // refresh ticket
-+ arraysetsorted.add(delayUnload);
-+ }
-+ }
-+ // Paper end
- }
-
- if (arraysetsorted.isEmpty()) {
-diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/Ticket.java
-+++ b/src/main/java/net/minecraft/server/level/Ticket.java
-@@ -0,0 +0,0 @@ public final class Ticket implements Comparable> {
- private final int ticketLevel;
- public final T key;
- public long createdTick;
-+ public long delayUnloadBy; // Paper
-
- protected Ticket(TicketType type, int level, T argument) {
- this.type = type;
- this.ticketLevel = level;
- this.key = argument;
-+ this.delayUnloadBy = type.timeout; // Paper
- }
-
- @Override
-@@ -0,0 +0,0 @@ public final class Ticket implements Comparable> {
- }
-
- protected boolean timedOut(long currentTick) {
-- long l = this.type.timeout();
-+ long l = delayUnloadBy; // Paper
- return l != 0L && currentTick - this.createdTick > l;
- }
- }
-diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/TicketType.java
-+++ b/src/main/java/net/minecraft/server/level/TicketType.java
-@@ -0,0 +0,0 @@ public class TicketType {
- public static final TicketType UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
- public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
- public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
-+ public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper
-
- public static TicketType create(String name, Comparator argumentComparator) {
- return new TicketType<>(name, argumentComparator, 0L);
diff --git a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
index 289e89a58b..09aa16c7c4 100644
--- a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
+++ b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
@@ -571,7 +571,7 @@ diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
log.log( Level.SEVERE, "During the run of the server, a plugin set an excessive velocity on an entity" );
log.log( Level.SEVERE, "This may be the cause of the issue, or it may be entirely unrelated" );
log.log( Level.SEVERE, org.bukkit.craftbukkit.CraftServer.excessiveVelEx.getMessage());
@@ -580,7 +580,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
{
log.log( Level.SEVERE, "\t\t" + stack );
}
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
}
log.log( Level.SEVERE, "\tStack:" );
//
diff --git a/patches/server/Detail-more-information-in-watchdog-dumps.patch b/patches/server/Detail-more-information-in-watchdog-dumps.patch
index c7a928dfd0..084b89e786 100644
--- a/patches/server/Detail-more-information-in-watchdog-dumps.patch
+++ b/patches/server/Detail-more-information-in-watchdog-dumps.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Thu, 26 Mar 2020 21:59:32 -0700
Subject: [PATCH] Detail more information in watchdog dumps
@@ -208,7 +208,7 @@ diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
private volatile long lastTick;
private volatile boolean stopping;
@@ -287,11 +287,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
private WatchdogThread(long timeoutTime, boolean restart)
{
super( "Paper Watchdog Thread" );
-@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
+@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
- com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system
+ this.dumpTickingInfo(); // Paper - log detailed tick information
- WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log );
+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//
diff --git a/patches/server/Distance-manager-tick-timings.patch b/patches/server/Distance-manager-tick-timings.patch
index 56f081dabc..12146dec07 100644
--- a/patches/server/Distance-manager-tick-timings.patch
+++ b/patches/server/Distance-manager-tick-timings.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Sat, 18 Jul 2020 16:03:57 -0700
Subject: [PATCH] Distance manager tick timings
@@ -18,23 +18,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks");
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- public boolean runDistanceManagerUpdates() {
- if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority
- if (this.chunkMap.unloadingPlayerChunk) { LOGGER.error("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Paper
-+ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager
- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
- boolean flag1 = this.chunkMap.promoteChunkMap();
+--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+@@ -0,0 +0,0 @@ public final class ChunkHolderManager {
+ }
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- this.clearCache();
- return true;
- }
+ public boolean processTicketUpdates() {
++ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager
+ return this.processTicketUpdates(true, true, null);
+ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager
}
- // Paper start
+ private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
diff --git a/patches/server/Do-not-allow-the-server-to-unload-chunks-at-request-.patch b/patches/server/Do-not-allow-the-server-to-unload-chunks-at-request-.patch
deleted file mode 100644
index b0805af230..0000000000
--- a/patches/server/Do-not-allow-the-server-to-unload-chunks-at-request-.patch
+++ /dev/null
@@ -1,23 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Thu, 11 Mar 2021 02:32:30 -0800
-Subject: [PATCH] Do not allow the server to unload chunks at request of
- plugins
-
-In general the chunk system is not well suited for this behavior,
-especially if it is called during a chunk load. The chunks pushed
-to be unloaded will simply be unloaded next tick, rather than
-immediately.
-
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-
- // CraftBukkit start - modelled on below
- public void purgeUnload() {
-+ if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system
- this.level.getProfiler().push("purge");
- this.distanceManager.purgeStaleTickets();
- this.runDistanceManagerUpdates();
diff --git a/patches/server/Do-not-allow-ticket-level-changes-when-updating-chun.patch b/patches/server/Do-not-allow-ticket-level-changes-when-updating-chun.patch
deleted file mode 100644
index f23aadf61a..0000000000
--- a/patches/server/Do-not-allow-ticket-level-changes-when-updating-chun.patch
+++ /dev/null
@@ -1,40 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sun, 20 Jun 2021 00:08:13 -0700
-Subject: [PATCH] Do not allow ticket level changes when updating chunk ticking
- state
-
-This WILL cause state corruption if it happens. So, don't
-allow it.
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -0,0 +0,0 @@ public class ChunkHolder {
- CompletableFuture completablefuture1 = new CompletableFuture();
-
- completablefuture1.thenRunAsync(() -> {
-+ // Paper start - do not allow ticket level changes
-+ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk;
-+ this.chunkMap.unloadingPlayerChunk = true;
-+ try {
-+ // Paper end - do not allow ticket level changes
- playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
-+ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes
- }, executor);
- this.pendingFullStateConfirmation = completablefuture1;
- completablefuture.thenAccept((either) -> {
-@@ -0,0 +0,0 @@ public class ChunkHolder {
-
- private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) {
- this.pendingFullStateConfirmation.cancel(false);
-+ // Paper start - do not allow ticket level changes
-+ boolean unloadingBefore = this.chunkMap.unloadingPlayerChunk;
-+ this.chunkMap.unloadingPlayerChunk = true;
-+ try { // Paper end - do not allow ticket level changes
- playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state);
-+ } finally { this.chunkMap.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes
- }
-
- protected long updateCount; // Paper - correctly handle recursion
diff --git a/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch b/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
deleted file mode 100644
index a27ee26750..0000000000
--- a/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sat, 19 Sep 2020 15:29:16 -0700
-Subject: [PATCH] Do not allow ticket level changes while unloading
- playerchunks
-
-Sync loading the chunk at this stage would cause it to load
-older data, as well as screwing our region state.
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- // Paper end
-
-+ boolean unloadingPlayerChunk = false; // Paper - do not allow ticket level changes while unloading chunks
- public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) {
- super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
- // Paper - don't copy
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- @Nullable
- ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
-+ if (this.unloadingPlayerChunk) { net.minecraft.server.MinecraftServer.LOGGER.error("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Paper
- if (k > ChunkMap.MAX_CHUNK_DISTANCE && level > ChunkMap.MAX_CHUNK_DISTANCE) {
- return holder;
- } else {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- if (completablefuture1 != completablefuture) {
- this.scheduleUnload(pos, holder);
- } else {
-+ // Paper start - do not allow ticket level changes while unloading chunks
-+ org.spigotmc.AsyncCatcher.catchOp("playerchunk unload");
-+ boolean unloadingBefore = this.unloadingPlayerChunk;
-+ this.unloadingPlayerChunk = true;
-+ try {
-+ // Paper end - do not allow ticket level changes while unloading chunks
- // Paper start
- boolean removed;
- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- } else if (removed) { // Paper start
- net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder);
- } // Paper end
-+ } finally { this.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes while unloading chunks
-
- }
- };
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-
- public boolean runDistanceManagerUpdates() {
- if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority
-+ if (this.chunkMap.unloadingPlayerChunk) { LOGGER.error("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Paper
- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
- boolean flag1 = this.chunkMap.promoteChunkMap();
-
diff --git a/patches/server/Do-not-copy-visible-chunks.patch b/patches/server/Do-not-copy-visible-chunks.patch
deleted file mode 100644
index 3167cd480b..0000000000
--- a/patches/server/Do-not-copy-visible-chunks.patch
+++ /dev/null
@@ -1,122 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sun, 21 Mar 2021 11:22:10 -0700
-Subject: [PATCH] Do not copy visible chunks
-
-For servers with a lot of chunk holders, copying for each
-tickDistanceManager call can take up quite a bit in
-the function. I saw approximately 1/3rd of the function
-on the copy.
-
-diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/ChunkSystem.java
-+++ b/src/main/java/net/minecraft/server/ChunkSystem.java
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
- }
-
- public static List getVisibleChunkHolders(final ServerLevel level) {
-- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
-+ if (Bukkit.isPrimaryThread()) {
-+ return level.chunkSource.chunkMap.updatingChunks.getVisibleValuesCopy();
-+ }
-+ synchronized (level.chunkSource.chunkMap.updatingChunks) {
-+ return level.chunkSource.chunkMap.updatingChunks.getVisibleValuesCopy();
-+ }
- }
-
- public static List getUpdatingChunkHolders(final ServerLevel level) {
-- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
-+ return level.chunkSource.chunkMap.updatingChunks.getUpdatingValuesCopy();
- }
-
- public static int getVisibleChunkHolderCount(final ServerLevel level) {
-- return level.chunkSource.chunkMap.visibleChunkMap.size();
-+ return level.chunkSource.chunkMap.updatingChunks.getVisibleMap().size();
- }
-
- public static int getUpdatingChunkHolderCount(final ServerLevel level) {
-- return level.chunkSource.chunkMap.updatingChunkMap.size();
-+ return level.chunkSource.chunkMap.updatingChunks.getUpdatingMap().size();
- }
-
- public static boolean hasAnyChunkHolders(final ServerLevel level) {
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private static final int MIN_VIEW_DISTANCE = 3;
- public static final int MAX_VIEW_DISTANCE = 33;
- public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance();
-+ // Paper start - Don't copy
-+ public final com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object updatingChunks = new com.destroystokyo.paper.util.map.QueuedChangesMapLong2Object<>();
-+ // Paper end - Don't copy
- public static final int FORCED_TICKET_LEVEL = 31;
-- public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
-- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap;
-+ // Paper - Don't copy
- private final Long2ObjectLinkedOpenHashMap pendingUnloads;
- public final LongSet entitiesInLevel;
- public final ServerLevel level;
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) {
- super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
-- this.visibleChunkMap = this.updatingChunkMap.clone();
-+ // Paper - don't copy
- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
- this.entitiesInLevel = new LongOpenHashSet();
- this.toDrop = new LongOpenHashSet();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- @Nullable
- public ChunkHolder getUpdatingChunkIfPresent(long pos) {
-- return (ChunkHolder) this.updatingChunkMap.get(pos);
-+ return this.updatingChunks.getUpdating(pos); // Paper - Don't copy
- }
-
- @Nullable
- public ChunkHolder getVisibleChunkIfPresent(long pos) {
-- return (ChunkHolder) this.visibleChunkMap.get(pos);
-+ // Paper start - Don't copy
-+ if (Thread.currentThread() == this.level.thread) {
-+ return this.updatingChunks.getVisible(pos);
-+ }
-+ return this.updatingChunks.getVisibleAsync(pos);
-+ // Paper end - Don't copy
- }
-
- protected IntSupplier getChunkQueueLevel(long pos) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- // Paper start
- holder.onChunkAdd();
- // Paper end
-- this.updatingChunkMap.put(pos, holder);
-+ this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
- this.modified = true;
- }
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
- long j = longiterator.nextLong();
-- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
-+ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
-
- if (playerchunk != null) {
- playerchunk.onChunkRemove(); // Paper
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- if (!this.modified) {
- return false;
- } else {
-- this.visibleChunkMap = this.updatingChunkMap.clone();
-+ // Paper start - Don't copy
-+ synchronized (this.updatingChunks) {
-+ this.updatingChunks.performUpdates();
-+ }
-+ // Paper end - Don't copy
-+
- this.modified = false;
- return true;
- }
diff --git a/patches/server/Do-not-process-entity-loads-in-CraftChunk-getEntitie.patch b/patches/server/Do-not-process-entity-loads-in-CraftChunk-getEntitie.patch
deleted file mode 100644
index 0a2c4303cc..0000000000
--- a/patches/server/Do-not-process-entity-loads-in-CraftChunk-getEntitie.patch
+++ /dev/null
@@ -1,68 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Shane Freeder
-Date: Fri, 3 Sep 2021 15:50:25 +0100
-Subject: [PATCH] Do not process entity loads in CraftChunk#getEntities
-
-This re-introduces the issue behind #5872 but fixes #6543
-The logic here is generally flawed however somewhat of a nuance,
-upstream uses managedBlock which is basically needed to process
-the posted entity adds, but, has the side-effect of processing any
-chunk loads which has the naunce of stacking up and either causing a
-massive performance hit, or can potentially lead the server to crash.
-
-This issue is particularly noticable on paper due to the cumulative efforts
-to drastically improve chunk loading speeds which means that there is much more
-of a chance that we're about to eat a dirtload of chunk load callbacks, thus
-making this issue much more of an issue
-
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-@@ -0,0 +0,0 @@ public class CraftChunk implements Chunk {
- this.getWorld().getChunkAt(x, z); // Transient load for this tick
- }
-
-- PersistentEntitySectionManager entityManager = this.getCraftWorld().getHandle().entityManager;
-- long pair = ChunkPos.asLong(x, z);
--
-- if (entityManager.areEntitiesLoaded(pair)) {
-- return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this
-- }
--
-- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
--
-- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
-- ProcessorMailbox mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
-- BooleanSupplier supplier = () -> {
-- // only execute inbox if our entities are not present
-- if (entityManager.areEntitiesLoaded(pair)) {
-- return true;
-- }
--
-- if (!entityManager.isPending(pair)) {
-- // Our entities got unloaded, this should normally not happen.
-- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
-- }
--
-- // tick loading inbox, which loads the created entities to the world
-- // (if present)
-- entityManager.tick();
-- // check if our entities are loaded
-- return entityManager.areEntitiesLoaded(pair);
-- };
--
-- // now we wait until the entities are loaded,
-- // the converting from NBT to entity object is done on the main Thread which is why we wait
-- while (!supplier.getAsBoolean()) {
-- if (mailbox.size() != 0) {
-- mailbox.run();
-- } else {
-- Thread.yield();
-- LockSupport.parkNanos("waiting for entity loading", 100000L);
-- }
-- }
--
- return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this
- }
-
diff --git a/patches/server/Don-t-crash-if-player-is-attempted-to-be-removed-fro.patch b/patches/server/Don-t-crash-if-player-is-attempted-to-be-removed-fro.patch
index c143fab0de..ff04083261 100644
--- a/patches/server/Don-t-crash-if-player-is-attempted-to-be-removed-fro.patch
+++ b/patches/server/Don-t-crash-if-player-is-attempted-to-be-removed-fro.patch
@@ -20,4 +20,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ if (objectset == null || objectset.isEmpty()) { // Paper
this.playersPerChunk.remove(i);
this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
- this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
diff --git a/patches/server/Don-t-lookup-fluid-state-when-raytracing.patch b/patches/server/Don-t-lookup-fluid-state-when-raytracing.patch
index 91b1f8cd47..eb5a9b30ca 100644
--- a/patches/server/Don-t-lookup-fluid-state-when-raytracing.patch
+++ b/patches/server/Don-t-lookup-fluid-state-when-raytracing.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Fri, 28 Aug 2020 12:33:47 -0700
Subject: [PATCH] Don't lookup fluid state when raytracing
diff --git a/patches/server/Don-t-mark-dirty-in-invalid-locations-SPIGOT-6086.patch b/patches/server/Don-t-mark-dirty-in-invalid-locations-SPIGOT-6086.patch
deleted file mode 100644
index eea7d66488..0000000000
--- a/patches/server/Don-t-mark-dirty-in-invalid-locations-SPIGOT-6086.patch
+++ /dev/null
@@ -1,18 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Mariell Hoversholm
-Date: Sun, 27 Sep 2020 16:25:24 +0200
-Subject: [PATCH] Don't mark dirty in invalid locations (SPIGOT-6086)
-
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -0,0 +0,0 @@ public class ChunkHolder {
- }
-
- public void blockChanged(BlockPos pos) {
-+ if (!pos.isInsideBuildHeightAndWorldBoundsHorizontal(levelHeightAccessor)) return; // Paper - SPIGOT-6086 for all invalid locations; avoid acquiring locks
- LevelChunk chunk = this.getTickingChunk();
-
- if (chunk != null) {
diff --git a/patches/server/Don-t-tick-markers.patch b/patches/server/Don-t-tick-markers.patch
index 5c89e953ef..a66804f613 100644
--- a/patches/server/Don-t-tick-markers.patch
+++ b/patches/server/Don-t-tick-markers.patch
@@ -31,8 +31,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public void onTickingStart(Entity entity) {
+ if (entity instanceof net.minecraft.world.entity.Marker) return; // Paper - Don't tick markers
ServerLevel.this.entityTickList.add(entity);
- ServerLevel.this.entityManager.addNavigatorsIfPathingToRegion(entity); // Paper - optimise notify
}
+
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/spigotmc/ActivationRange.java
diff --git a/patches/server/Duplicate-UUID-Resolve-Option.patch b/patches/server/Duplicate-UUID-Resolve-Option.patch
index a11fbc6ada..be52b8a7e0 100644
--- a/patches/server/Duplicate-UUID-Resolve-Option.patch
+++ b/patches/server/Duplicate-UUID-Resolve-Option.patch
@@ -41,9 +41,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
-
++ // Paper start - duplicate uuid resolving
+ if (net.minecraft.server.level.ChunkMap.checkDupeUUID(level, entity)) {
+ return;
+ }
++ if (net.minecraft.world.level.Level.DEBUG_ENTITIES && ((Entity) entity).level.paperConfig().entities.spawning.duplicateUuid.mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.NOTHING) {
++ if (((Entity) entity).addedToWorldStack != null) {
++ ((Entity) entity).addedToWorldStack.printStackTrace();
++ }
++ net.minecraft.server.level.ServerLevel.getAddToWorldStackTrace((Entity) entity).printStackTrace();
++ }
++ // Paper end - duplicate uuid resolving
}
public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
@@ -60,7 +68,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}));
// CraftBukkit end
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
+ // Paper start
@@ -107,32 +115,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ // Paper end
public CompletableFuture> prepareTickingChunk(ChunkHolder holder) {
- ChunkPos chunkcoordintpair = holder.getPos();
- CompletableFuture, ChunkHolder.ChunkLoadingFailure>> completablefuture = this.getChunkRangeFuture(chunkcoordintpair, 1, (i) -> {
-diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A
-
- private boolean addEntityUuid(T entity) {
- if (!this.knownUuids.add(entity.getUUID())) {
-+ // Paper start
-+ T conflict = this.visibleEntityStorage.getEntity(entity.getUUID());
-+ if (conflict != null && ((Entity) conflict).isRemoved()) {
-+ stopTracking(conflict); // remove the existing entity
-+ return true;
-+ }
-+ // Paper end
- PersistentEntitySectionManager.LOGGER.warn("UUID of added entity already exists: {}", entity);
-+ // Paper start
-+ if (net.minecraft.world.level.Level.DEBUG_ENTITIES && ((Entity) entity).level.paperConfig().entities.spawning.duplicateUuid.mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.NOTHING) {
-+ if (((Entity) entity).addedToWorldStack != null) {
-+ ((Entity) entity).addedToWorldStack.printStackTrace();
-+ }
-+ net.minecraft.server.level.ServerLevel.getAddToWorldStackTrace((Entity) entity).printStackTrace();
-+ }
-+ // Paper end
- return false;
- } else {
- return true;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
diff --git a/patches/server/Ensure-Entity-AABB-s-are-never-invalid.patch b/patches/server/Ensure-Entity-AABB-s-are-never-invalid.patch
index d76f58368c..9242d2a605 100644
--- a/patches/server/Ensure-Entity-AABB-s-are-never-invalid.patch
+++ b/patches/server/Ensure-Entity-AABB-s-are-never-invalid.patch
@@ -28,9 +28,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
+ // Paper end
- if (this.position.x != x || this.position.y != y || this.position.z != z) {
- this.position = new Vec3(x, y, z);
- int i = Mth.floor(x);
+ // Paper start - rewrite chunk system
+ if (this.updatingSectionStatus) {
+ LOGGER.error("Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z) + " since it is processing a section status update", new Throwable());
@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
this.levelCallback.onMove();
}
diff --git a/patches/server/Entity-Activation-Range-2.0.patch b/patches/server/Entity-Activation-Range-2.0.patch
index e7de29b93b..05b6e7dc24 100644
--- a/patches/server/Entity-Activation-Range-2.0.patch
+++ b/patches/server/Entity-Activation-Range-2.0.patch
@@ -557,8 +557,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange );
+ // Paper end
- world.getEntities().get(maxBB, ActivationRange::activateEntity);
- }
+ // Paper start
+ java.util.List entities = world.getEntities((Entity)null, maxBB, null);
@@ -0,0 +0,0 @@ public class ActivationRange
* @param entity
* @return
@@ -621,8 +621,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
{
- return true;
+ return 20; // Paper
- }
-- if ( entity instanceof Villager && ( (Villager) entity ).canBreed() )
++ }
+ // Paper start
+ if (entity instanceof Bee) {
+ Bee bee = (Bee)entity;
@@ -650,7 +649,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return config.villagersWorkImmunityFor;
+ }
+ }
-+ }
+ }
+- if ( entity instanceof Villager && ( (Villager) entity ).canBreed() )
+ if ( entity instanceof Llama && ( (Llama) entity ).inCaravan() )
{
- return true;
@@ -678,11 +678,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // Paper start
+ if (entity instanceof Mob && ((Mob) entity).targetSelector.hasTasks() ) {
+ return 0;
-+ }
+ }
+ if (entity instanceof Pillager) {
+ Pillager pillager = (Pillager) entity;
+ // TODO:?
- }
++ }
+ // Paper end
}
// SPIGOT-6644: Otherwise the target refresh tick will be missed
diff --git a/patches/server/Entity-load-save-limit-per-chunk.patch b/patches/server/Entity-load-save-limit-per-chunk.patch
index ac8331526f..97b95ce3a4 100644
--- a/patches/server/Entity-load-save-limit-per-chunk.patch
+++ b/patches/server/Entity-load-save-limit-per-chunk.patch
@@ -38,21 +38,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
@@ -0,0 +0,0 @@ public class EntityStorage implements EntityPersistentStorage {
-
- } else {
- ListTag listTag = new ListTag();
-+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper
- dataList.getEntities().forEach((entity) -> {
-+ // Paper start
-+ final EntityType> entityType = entity.getType();
-+ final int saveLimit = this.level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
-+ if (saveLimit > -1) {
-+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) {
-+ return;
-+ }
-+ savedEntityCounts.merge(entityType, 1, Integer::sum);
+ return null;
+ }
+ ListTag listTag = new ListTag();
++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper
+ entities.forEach((entity) -> { // diff here: use entities parameter
++ // Paper start
++ final EntityType> entityType = entity.getType();
++ final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
++ if (saveLimit > -1) {
++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) {
++ return;
+ }
-+ // Paper end
- CompoundTag compoundTag = new CompoundTag();
- if (entity.save(compoundTag)) {
- listTag.add(compoundTag);
++ savedEntityCounts.merge(entityType, 1, Integer::sum);
++ }
++ // Paper end
+ CompoundTag compoundTag = new CompoundTag();
+ if (entity.save(compoundTag)) {
+ listTag.add(compoundTag);
diff --git a/patches/server/Execute-chunk-tasks-mid-tick.patch b/patches/server/Execute-chunk-tasks-mid-tick.patch
index 4899362e6a..2204eb204c 100644
--- a/patches/server/Execute-chunk-tasks-mid-tick.patch
+++ b/patches/server/Execute-chunk-tasks-mid-tick.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Mon, 6 Apr 2020 04:20:44 -0700
Subject: [PATCH] Execute chunk tasks mid-tick
@@ -119,7 +119,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
LevelChunk chunk1 = iterator1.next();
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
+ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - the chunk is known ticking
this.level.tickChunk(chunk1, k);
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
}
diff --git a/patches/server/Expose-client-protocol-version-and-virtual-host.patch b/patches/server/Expose-client-protocol-version-and-virtual-host.patch
index d78a465b7b..fe848be9b2 100644
--- a/patches/server/Expose-client-protocol-version-and-virtual-host.patch
+++ b/patches/server/Expose-client-protocol-version-and-virtual-host.patch
@@ -64,9 +64,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/network/Connection.java
+++ b/src/main/java/net/minecraft/network/Connection.java
@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
- private float averageSentPackets;
- private int tickCount;
- private boolean handlingFault;
+ }
+ }
+ // Paper end - add pending task queue
+ // Paper start - NetworkClient implementation
+ public int protocolVersion;
+ public java.net.InetSocketAddress virtualHost;
diff --git a/patches/server/Fix-AdvancementDataPlayer-leak-due-from-quitting-ear.patch b/patches/server/Fix-AdvancementDataPlayer-leak-due-from-quitting-ear.patch
index ebb71f1fa8..92be8d7628 100644
--- a/patches/server/Fix-AdvancementDataPlayer-leak-due-from-quitting-ear.patch
+++ b/patches/server/Fix-AdvancementDataPlayer-leak-due-from-quitting-ear.patch
@@ -1,5 +1,5 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
+From: Spottedleaf
Date: Mon, 13 Jul 2020 06:22:54 -0700
Subject: [PATCH] Fix AdvancementDataPlayer leak due from quitting early in
login
diff --git a/patches/server/Fix-Chunk-Post-Processing-deadlock-risk.patch b/patches/server/Fix-Chunk-Post-Processing-deadlock-risk.patch
deleted file mode 100644
index f3100bfb6e..0000000000
--- a/patches/server/Fix-Chunk-Post-Processing-deadlock-risk.patch
+++ /dev/null
@@ -1,73 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar
-Date: Sat, 18 Apr 2020 04:36:11 -0400
-Subject: [PATCH] Fix Chunk Post Processing deadlock risk
-
-See: https://gist.github.com/aikar/dd22bbd2a3d78a2fd3d92e95e9f28dc6
-
-as part of post processing a chunk, we can call ChunkConverter.
-
-ChunkConverter then kicks off major physics updates, and when blocks
-that have connections across chunk boundaries occur, a recursive risk
-can occur where A updates a block that triggers a physics request.
-
-That physics request may trigger a chunk request, that then enqueues
-a task into the Mailbox ChunkTaskQueueSorter.
-
-If anything requests that same chunk that is in the middle of conversion,
-it's mailbox queue is going to be held up, so the subsequent chunk request
-will be unable to proceed.
-
-We delay post processing of Chunk.A() 1 "pass" by re stuffing it back into
-the executor so that the mailbox ChunkQueue is now considered empty.
-
-This successfully fixed a reoccurring and highly reproducible crash
-for heightmaps.
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- };
- // CraftBukkit end
-
-+ final CallbackExecutor chunkLoadConversionCallbackExecutor = new CallbackExecutor(); // Paper
- // Paper start - distance maps
- private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
-
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- });
- CompletableFuture> completablefuture1 = completablefuture.thenApplyAsync((either) -> {
- return either.mapLeft((list) -> {
-- return (LevelChunk) list.get(list.size() / 2);
-- });
-- }, (runnable) -> {
-- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
-- }).thenApplyAsync((either) -> {
-- return either.ifLeft((chunk) -> {
-+ // Paper start - revert 1.18.2 diff
-+ final LevelChunk chunk = (LevelChunk) list.get(list.size() / 2);
- chunk.postProcessGeneration();
- this.level.startTickingChunk(chunk);
-+ return chunk;
- });
-- }, this.mainThreadExecutor);
-+ }, (runnable) -> {
-+ this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, () -> ChunkMap.this.chunkLoadConversionCallbackExecutor.execute(runnable))); // Paper - delay running Chunk post processing until outside of the sorter to prevent a deadlock scenario when post processing causes another chunk request.
-+ }); // Paper end - revert 1.18.2 diff
-
- completablefuture1.thenAcceptAsync((either) -> {
- either.ifLeft((chunk) -> {
-diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
- return super.pollTask() || execChunkTask; // Paper
- }
- } finally {
-+ chunkMap.chunkLoadConversionCallbackExecutor.run(); // Paper - Add chunk load conversion callback executor to prevent deadlock due to recursion in the chunk task queue sorter
- chunkMap.callbackExecutor.run();
- }
- // CraftBukkit end
diff --git a/patches/server/Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch b/patches/server/Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch
deleted file mode 100644
index 5f90d1d0b7..0000000000
--- a/patches/server/Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch
+++ /dev/null
@@ -1,43 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Mon, 13 May 2019 21:10:59 -0700
-Subject: [PATCH] Fix CraftServer#isPrimaryThread and MinecraftServer
- isMainThread
-
-md_5 changed it so he could shut down the server asynchronously
-from watchdog, although we have patches that prevent that type
-of behavior for this exact reason.
-
-md_5 also placed code in PlayerConnectionUtils that would have
-solved https://bugs.mojang.com/browse/MC-142590, making the change
-to MinecraftServer#isMainThread irrelevant.
-By reverting his change to MinecraftServer#isMainThread packet
-handling that should have been handled synchronously will be handled
-synchronously when the server gets shut down.
-
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop