From 0f1a8717e84a4a07e51f9042f37720d0d470b437 Mon Sep 17 00:00:00 2001
From: Spottedleaf <6100722+Spottedleaf@users.noreply.github.com>
Date: Mon, 26 Sep 2022 01:02:51 -0700
Subject: [PATCH] Rewrite chunk system (#8177)
Patch documentation to come
Issues with the old system that are fixed now:
- World generation does not scale with cpu cores effectively.
- Relies on the main thread for scheduling and maintaining chunk state, dropping chunk load/generate rates at lower tps.
- Unreliable prioritisation of chunk gen/load calls that block the main thread.
- Shutdown logic is utterly unreliable, as it has to wait for all chunks to unload - is it guaranteed that the chunk system is in a state on shutdown that it can reliably do this? Watchdog shutdown also typically failed due to thread checks, which is now resolved.
- Saving of data is not unified (i.e can save chunk data without saving entity data, poses problems for desync if shutdown is really abnormal.
- Entities are not loaded with chunks. This caused quite a bit of headache for Chunk#getEntities API, but now the new chunk system loads entities with chunks so that they are ready whenever the chunk loads in. Effectively brings the behavior back to 1.16 era, but still storing entities in their own separate regionfiles.
The above list is not complete. The patch documentation will complete it.
New chunk system hard relies on starlight and dataconverter, and most importantly the new concurrent utilities in ConcurrentUtil.
Some of the old async chunk i/o interface (i.e the old file io thread reroutes _some_ calls to the new file io thread) is kept for plugin compat reasons. It will be removed in the next major version of minecraft.
The old legacy chunk system patches have been moved to the removed folder in case we need them again.
---
patches/server/Actually-unload-POI-data.patch | 321 -
patches/server/Add-API-for-quit-reason.patch | 2 +-
...-replace-OfflinePlayer-getLastPlayed.patch | 2 +-
...nate-Current-redstone-implementation.patch | 2 +-
...dd-Early-Warning-Feature-to-WatchDog.patch | 10 +-
.../Add-debug-for-sync-chunk-loads.patch | 14 +-
.../Add-exception-reporting-event.patch | 12 -
patches/server/Add-more-async-catchers.patch | 44 -
.../server/Add-packet-limiter-config.patch | 2 +-
...aper-mobcaps-and-paper-playermobcaps.patch | 2 +-
patches/server/Add-velocity-warnings.patch | 4 +-
...trolled-flushing-for-network-manager.patch | 4 +-
...dition-of-entities-to-entity-ticklis.patch | 89 -
patches/server/Anti-Xray.patch | 34 +-
.../Async-GameProfileCache-saving.patch | 2 +-
...difications-to-critical-entity-state.patch | 12 +-
.../Asynchronous-chunk-IO-and-loading.patch | 3516 ---
...culate-regionfile-header-if-it-is-co.patch | 2 +-
...opper-searches-if-there-are-no-items.patch | 124 -
...arseException-in-Entity-and-TE-names.patch | 4 +-
patches/server/Chunk-Save-Reattempt.patch | 4 +-
patches/server/Chunk-debug-command.patch | 432 -
patches/server/ChunkMapDistance-CME.patch | 84 -
patches/server/ConcurrentUtil.patch | 158 +-
...ush-calls-for-entity-tracker-packets.patch | 2 +-
...le-recursion-for-chunkholder-updates.patch | 37 -
...unk-Unloads-based-on-Player-Movement.patch | 89 -
...ktraces-in-log-messages-crash-report.patch | 4 +-
...l-more-information-in-watchdog-dumps.patch | 10 +-
.../Distance-manager-tick-timings.patch | 26 +-
...-server-to-unload-chunks-at-request-.patch | 23 -
...ket-level-changes-when-updating-chun.patch | 40 -
...ket-level-changes-while-unloading-pl.patch | 62 -
.../server/Do-not-copy-visible-chunks.patch | 122 -
...ntity-loads-in-CraftChunk-getEntitie.patch | 68 -
...layer-is-attempted-to-be-removed-fro.patch | 2 +-
...t-lookup-fluid-state-when-raytracing.patch | 2 +-
...rty-in-invalid-locations-SPIGOT-6086.patch | 18 -
patches/server/Don-t-tick-markers.patch | 2 +-
.../Duplicate-UUID-Resolve-Option.patch | 41 +-
...sure-Entity-AABB-s-are-never-invalid.patch | 6 +-
.../server/Entity-Activation-Range-2.0.patch | 14 +-
.../Entity-load-save-limit-per-chunk.patch | 34 +-
.../server/Execute-chunk-tasks-mid-tick.patch | 4 +-
...nt-protocol-version-and-virtual-host.patch | 6 +-
...ataPlayer-leak-due-from-quitting-ear.patch | 2 +-
...-Chunk-Post-Processing-deadlock-risk.patch | 73 -
...isPrimaryThread-and-MinecraftServer-.patch | 43 -
.../Fix-GameProfileCache-concurrency.patch | 2 +-
patches/server/Fix-Light-Command.patch | 186 -
.../Fix-World-isChunkGenerated-calls.patch | 46 +-
...chunks-refusing-to-unload-at-low-TPS.patch | 26 -
...r-large-move-vectors-crashing-server.patch | 2 +-
.../Fix-save-problems-on-shutdown.patch | 74 -
...Fix-some-rails-connecting-improperly.patch | 2 +-
.../Further-improve-server-tick-loop.patch | 15 +-
...ard-against-invalid-entity-positions.patch | 4 +-
...single-and-multi-AABB-VoxelShapes-an.patch | 11 +-
...k-Priority-Urgency-System-for-Chunks.patch | 1254 --
.../Implement-Player-Client-Options-API.patch | 4 +-
...mprove-Chunk-Status-Transition-Speed.patch | 81 -
.../Improve-boat-collision-performance.patch | 2 +-
...inig-for-some-hot-IBlockData-methods.patch | 6 +-
.../server/Improved-Watchdog-Support.patch | 46 +-
...Load-Chunks-for-Login-Asynchronously.patch | 2 +-
...og-when-the-async-catcher-is-tripped.patch | 2 +-
.../MC-4-Fix-item-position-desync.patch | 6 +-
patches/server/MC-Utils.patch | 195 +-
.../Make-CallbackExecutor-strict-again.patch | 48 -
...d-getChunkAt-has-inlined-logic-for-l.patch | 2 +-
...more-aggressive-in-the-chunk-unload-.patch | 36 -
...ally-inline-methods-in-BlockPosition.patch | 2 +-
...uler-threads-according-to-the-plugin.patch | 2 +-
...on-Wither-Death-sounds-to-same-world.patch | 10 +-
...primise-map-impl-for-tracked-players.patch | 8 -
.../Optimise-ArraySetSorted-removeIf.patch | 88 -
...timise-IEntityAccess-getPlayerByUUID.patch | 4 +-
.../server/Optimise-WorldServer-notify.patch | 337 -
.../Optimise-chunk-tick-iteration.patch | 20 +-
...on-checking-in-player-move-packet-ha.patch | 2 +-
.../server/Optimise-general-POI-access.patch | 11 +-
...e-getChunkAt-calls-for-loaded-chunks.patch | 2 +-
patches/server/Optimise-getType-calls.patch | 2 +-
.../Optimise-nearby-player-lookups.patch | 29 +-
.../Optimise-non-flush-packet-sending.patch | 2 +-
.../Optimize-Captured-TileEntity-Lookup.patch | 2 +-
.../Optimize-CraftBlockData-Creation.patch | 2 +-
...oalSelector-Goal.Flag-Set-operations.patch | 2 +-
...-Manager-and-add-advanced-packet-sup.patch | 8 +
...rLevels-chunk-level-checking-methods.patch | 69 -
...erCloseEnoughForSpawning-to-use-dist.patch | 60 +-
...ptimize-indirect-passenger-iteration.patch | 3 +-
...ldBounds-and-getBlockState-for-inlin.patch | 4 +-
patches/server/Paper-config-files.patch | 13 +-
patches/server/Paper-dumpitem-command.patch | 2 +-
...layer-View-Distance-API-placeholders.patch | 112 -
.../PlayerNaturallySpawnCreaturesEvent.patch | 2 +-
...alls-removing-tickets-for-sync-loads.patch | 80 -
...o-worldlist-before-initing-the-world.patch | 2 +-
.../Reduce-Either-Optional-allocation.patch | 2 +-
...bleInt-allocations-from-light-engine.patch | 2 +-
...llocation-of-Vec3D-by-entity-tracker.patch | 4 +-
...blockpos-allocation-from-pathfinding.patch | 2 +-
...e-memory-footprint-of-NBTTagCompound.patch | 2 +-
.../Remove-streams-for-villager-AI.patch | 2 +-
.../Replace-player-chunk-loader-system.patch | 2270 --
.../Replace-ticket-level-propagator.patch | 258 -
...ace-order-when-capturing-blockstates.patch | 2 +-
patches/server/Rewrite-chunk-system.patch | 18056 ++++++++++++++++
.../server/Rewrite-dataconverter-system.patch | 4 +-
...ite-entity-bounding-box-lookup-calls.patch | 1300 --
...ient-crashes-server-lists-and-Mojang.patch | 8 +-
...the-light-engine.patch => Starlight.patch} | 365 +-
...te-operations-for-updating-light-dat.patch | 2 +-
...tem-property-for-disabling-watchdoge.patch | 2 +-
patches/server/Time-scoreboard-search.patch | 2 +-
.../Use-a-Queue-for-Queueing-Commands.patch | 8 +-
...tance-map-to-optimise-entity-tracker.patch | 108 +-
.../server/WorldCreator-keepSpawnLoaded.patch | 2 +-
.../fix-converting-txt-to-json-file.patch | 2 +-
...ement-optional-per-player-mob-spawns.patch | 40 +-
.../incremental-chunk-and-player-saving.patch | 215 +-
122 files changed, 18921 insertions(+), 12279 deletions(-)
delete mode 100644 patches/server/Actually-unload-POI-data.patch
delete mode 100644 patches/server/Add-more-async-catchers.patch
delete mode 100644 patches/server/Allow-removal-addition-of-entities-to-entity-ticklis.patch
delete mode 100644 patches/server/Asynchronous-chunk-IO-and-loading.patch
delete mode 100644 patches/server/Avoid-hopper-searches-if-there-are-no-items.patch
delete mode 100644 patches/server/Chunk-debug-command.patch
delete mode 100644 patches/server/ChunkMapDistance-CME.patch
delete mode 100644 patches/server/Correctly-handle-recursion-for-chunkholder-updates.patch
delete mode 100644 patches/server/Delay-Chunk-Unloads-based-on-Player-Movement.patch
delete mode 100644 patches/server/Do-not-allow-the-server-to-unload-chunks-at-request-.patch
delete mode 100644 patches/server/Do-not-allow-ticket-level-changes-when-updating-chun.patch
delete mode 100644 patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
delete mode 100644 patches/server/Do-not-copy-visible-chunks.patch
delete mode 100644 patches/server/Do-not-process-entity-loads-in-CraftChunk-getEntitie.patch
delete mode 100644 patches/server/Don-t-mark-dirty-in-invalid-locations-SPIGOT-6086.patch
delete mode 100644 patches/server/Fix-Chunk-Post-Processing-deadlock-risk.patch
delete mode 100644 patches/server/Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch
delete mode 100644 patches/server/Fix-Light-Command.patch
delete mode 100644 patches/server/Fix-chunks-refusing-to-unload-at-low-TPS.patch
delete mode 100644 patches/server/Fix-save-problems-on-shutdown.patch
delete mode 100644 patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
delete mode 100644 patches/server/Improve-Chunk-Status-Transition-Speed.patch
delete mode 100644 patches/server/Make-CallbackExecutor-strict-again.patch
delete mode 100644 patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch
delete mode 100644 patches/server/Optimise-ArraySetSorted-removeIf.patch
delete mode 100644 patches/server/Optimise-WorldServer-notify.patch
delete mode 100644 patches/server/Optimize-ServerLevels-chunk-level-checking-methods.patch
delete mode 100644 patches/server/Per-Player-View-Distance-API-placeholders.patch
delete mode 100644 patches/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch
delete mode 100644 patches/server/Replace-player-chunk-loader-system.patch
delete mode 100644 patches/server/Replace-ticket-level-propagator.patch
create mode 100644 patches/server/Rewrite-chunk-system.patch
delete mode 100644 patches/server/Rewrite-entity-bounding-box-lookup-calls.patch
rename patches/server/{Rewrite-the-light-engine.patch => Starlight.patch} (95%)
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