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 +From: Spottedleaf Date: Sat, 11 Jul 2020 05:09:28 -0700 Subject: [PATCH] Fix GameProfileCache concurrency diff --git a/patches/server/Fix-Light-Command.patch b/patches/server/Fix-Light-Command.patch deleted file mode 100644 index 835378ed70..0000000000 --- a/patches/server/Fix-Light-Command.patch +++ /dev/null @@ -1,186 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 7 May 2020 19:17:36 -0400 -Subject: [PATCH] Fix Light Command - -This lets you run /paper fixlight (max 5) to automatically -fix all light data in the chunks. - -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.FixLightCommand; - import io.papermc.paper.command.subcommands.HeapDumpCommand; - import io.papermc.paper.command.subcommands.ReloadCommand; - import io.papermc.paper.command.subcommands.VersionCommand; -@@ -0,0 +0,0 @@ public final class PaperCommand extends Command { - commands.put(Set.of("reload"), new ReloadCommand()); - commands.put(Set.of("version"), new VersionCommand()); - commands.put(Set.of("debug", "chunkinfo"), new ChunkDebugCommand()); -+ commands.put(Set.of("fixlight"), new FixLightCommand()); - - 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/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.command.subcommands; -+ -+import io.papermc.paper.command.PaperSubcommand; -+import java.util.ArrayDeque; -+import java.util.Deque; -+import net.minecraft.server.MCUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.ThreadedLevelLightEngine; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.LevelChunk; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+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.GREEN; -+import static net.kyori.adventure.text.format.NamedTextColor.RED; -+ -+@DefaultQualifier(NonNull.class) -+public final class FixLightCommand implements PaperSubcommand { -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ this.doFixLight(sender, args); -+ return true; -+ } -+ -+ private void doFixLight(final CommandSender sender, final String[] args) { -+ if (!(sender instanceof Player)) { -+ sender.sendMessage(text("Only players can use this command", RED)); -+ return; -+ } -+ @Nullable Runnable post = null; -+ int radius = 2; -+ if (args.length > 0) { -+ try { -+ final int parsed = Integer.parseInt(args[0]); -+ if (parsed < 0) { -+ sender.sendMessage(text("Radius cannot be negative!", RED)); -+ return; -+ } -+ final int maxRadius = 5; -+ radius = Math.min(maxRadius, parsed); -+ if (radius != parsed) { -+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); -+ } -+ } catch (final Exception e) { -+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); -+ return; -+ } -+ } -+ -+ CraftPlayer player = (CraftPlayer) sender; -+ ServerPlayer handle = player.getHandle(); -+ ServerLevel world = (ServerLevel) handle.level; -+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); -+ -+ net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation()); -+ Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); -+ updateLight(sender, world, lightengine, queue, post); -+ } -+ -+ private void updateLight( -+ final CommandSender sender, -+ final ServerLevel world, -+ final ThreadedLevelLightEngine lightengine, -+ final Deque queue, -+ final @Nullable Runnable done -+ ) { -+ @Nullable ChunkPos coord = queue.poll(); -+ if (coord == null) { -+ sender.sendMessage(text("All Chunks Light updated", GREEN)); -+ if (done != null) { -+ done.run(); -+ } -+ return; -+ } -+ world.getChunkSource().getChunkAtAsynchronously(coord.x, coord.z, false, false).whenCompleteAsync((either, ex) -> { -+ if (ex != null) { -+ sender.sendMessage(text("Error loading chunk " + coord, RED)); -+ updateLight(sender, world, lightengine, queue, done); -+ return; -+ } -+ @Nullable LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null); -+ if (chunk == null) { -+ updateLight(sender, world, lightengine, queue, done); -+ return; -+ } -+ lightengine.setTaskPerBatch(world.paperConfig().misc.lightQueueSize + 16 * 256); // ensure full chunk can fit into queue -+ sender.sendMessage(text("Updating Light " + coord)); -+ int cx = chunk.getPos().x << 4; -+ int cz = chunk.getPos().z << 4; -+ for (int y = 0; y < world.getHeight(); y++) { -+ for (int x = 0; x < 16; x++) { -+ for (int z = 0; z < 16; z++) { -+ net.minecraft.core.BlockPos pos = new net.minecraft.core.BlockPos(cx + x, y, cz + z); -+ lightengine.checkBlock(pos); -+ } -+ } -+ } -+ lightengine.tryScheduleUpdate(); -+ @Nullable ChunkHolder visibleChunk = world.getChunkSource().chunkMap.getVisibleChunkIfPresent(chunk.coordinateKey); -+ if (visibleChunk != null) { -+ world.getChunkSource().chunkMap.addLightTask(visibleChunk, () -> { -+ MinecraftServer.getServer().processQueue.add(() -> { -+ visibleChunk.broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunk.getPos(), lightengine, null, null, true), false); -+ updateLight(sender, world, lightengine, queue, done); -+ }); -+ }); -+ } else { -+ updateLight(sender, world, lightengine, queue, done); -+ } -+ lightengine.setTaskPerBatch(world.paperConfig().misc.lightQueueSize); -+ }, MinecraftServer.getServer()); -+ } -+} -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 final ChunkTaskPriorityQueueSorter queueSorter; - private final ProcessorHandle> worldgenMailbox; - public final ProcessorHandle> mainThreadMailbox; -+ // Paper start -+ final ProcessorHandle> mailboxLight; -+ public void addLightTask(ChunkHolder playerchunk, Runnable run) { -+ this.mailboxLight.tell(ChunkTaskPriorityQueueSorter.message(playerchunk, run)); -+ } -+ // Paper end - public final ChunkProgressListener progressListener; - private final ChunkStatusUpdateListener chunkStatusListener; - public final ChunkMap.ChunkDistanceManager distanceManager; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - this.progressListener = worldGenerationProgressListener; - this.chunkStatusListener = chunkStatusChangeListener; -- ProcessorMailbox threadedmailbox1 = ProcessorMailbox.create(executor, "light"); -+ ProcessorMailbox lightthreaded; ProcessorMailbox threadedmailbox1 = lightthreaded = ProcessorMailbox.create(executor, "light"); // Paper - - this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE); - this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false); - this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false); -+ this.mailboxLight = this.queueSorter.getProcessor(lightthreaded, false);// Paper - this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false)); - this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); - this.overworldDataStorage = persistentStateManagerFactory; diff --git a/patches/server/Fix-World-isChunkGenerated-calls.patch b/patches/server/Fix-World-isChunkGenerated-calls.patch index 1c9f19b43f..8f8480138b 100644 --- a/patches/server/Fix-World-isChunkGenerated-calls.patch +++ b/patches/server/Fix-World-isChunkGenerated-calls.patch @@ -84,7 +84,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + + public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { -+ ChunkHolder chunkHolder = this.pendingUnloads.get(ChunkPos.asLong(chunkX, chunkZ)); ++ ChunkHolder chunkHolder = net.minecraft.server.ChunkSystem.getUnloadingChunkHolder(this.level, chunkX, chunkZ); + return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); + } + // Paper end @@ -92,50 +92,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) { // Spigot start return this.anyPlayerCloseEnoughForSpawning(pos, false); -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java -@@ -0,0 +0,0 @@ public class ChunkStatus { - return this.chunkType; - } - -+ // Paper start -+ public static ChunkStatus getStatus(String name) { -+ try { -+ // We need this otherwise we return EMPTY for invalid names -+ ResourceLocation key = new ResourceLocation(name); -+ return Registry.CHUNK_STATUS.getOptional(key).orElse(null); -+ } catch (Exception ex) { -+ return null; // invalid name -+ } -+ } -+ // Paper end - public static ChunkStatus byName(String id) { - return (ChunkStatus) Registry.CHUNK_STATUS.get(ResourceLocation.tryParse(id)); - } -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 { - })); - } - -+ // Paper start -+ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) { -+ if (compound == null) { -+ return null; -+ } -+ -+ // Note: Copied from below -+ return ChunkStatus.getStatus(compound.getString("Status")); -+ } -+ // Paper end -+ - public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) { - return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkStatus.ChunkType.PROTOCHUNK; - } 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 diff --git a/patches/server/Fix-chunks-refusing-to-unload-at-low-TPS.patch b/patches/server/Fix-chunks-refusing-to-unload-at-low-TPS.patch deleted file mode 100644 index 7a2e85f2a8..0000000000 --- a/patches/server/Fix-chunks-refusing-to-unload-at-low-TPS.patch +++ /dev/null @@ -1,26 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 1 Feb 2021 15:35:14 -0800 -Subject: [PATCH] Fix chunks refusing to unload at low TPS - -The full chunk future is appended to the chunk save future, but -when moving to unloaded ticket level it is not being completed with -the empty chunk access, so the chunk save must wait for the full -chunk future to complete. We can simply schedule to the immediate -executor to get this effect, rather than the main mailbox. - -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 chunk; - }); -- }, (runnable) -> { -- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); -- }); -+ }, this.mainThreadExecutor); // Paper - queue to execute immediately so this doesn't delay chunk unloading - } - - public int getTickingGenerated() { diff --git a/patches/server/Fix-for-large-move-vectors-crashing-server.patch b/patches/server/Fix-for-large-move-vectors-crashing-server.patch index 4371c97c06..58915421d8 100644 --- a/patches/server/Fix-for-large-move-vectors-crashing-server.patch +++ b/patches/server/Fix-for-large-move-vectors-crashing-server.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Sun, 17 May 2020 23:47:33 -0700 Subject: [PATCH] Fix for large move vectors crashing server diff --git a/patches/server/Fix-save-problems-on-shutdown.patch b/patches/server/Fix-save-problems-on-shutdown.patch deleted file mode 100644 index 505ef3de01..0000000000 --- a/patches/server/Fix-save-problems-on-shutdown.patch +++ /dev/null @@ -1,74 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 5 Mar 2022 17:12:52 -0800 -Subject: [PATCH] Fix save problems on shutdown - -- Save level.dat first, in case the shutdown is killed later -- Force run minecraftserver tasks and the chunk source tasks - while waiting for the chunk system to empty, as there's simply - too much trash that could prevent them from executing during - the chunk source tick (i.e "time left in tick" logic). -- Set forceTicks to true, so that player packets are always - processed so that the main process queue can be drained - -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 { - return worldserver1.getChunkSource().chunkMap.hasWork(); - })) { -@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - return true; - }, false); -+ while (worldserver.getChunkSource().pollTask()); // Paper - drain tasks - } - -- this.waitUntilNextTick(); -+ this.forceTicks = true; // Paper -+ while (this.pollTask()); // Paper - drain tasks - } - - this.saveAllChunks(false, true, false); -@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +From: Spottedleaf Date: Fri, 24 Jul 2020 15:56:05 -0700 Subject: [PATCH] Fix some rails connecting improperly diff --git a/patches/server/Further-improve-server-tick-loop.patch b/patches/server/Further-improve-server-tick-loop.patch index eef0ff54d3..bd90305098 100644 --- a/patches/server/Further-improve-server-tick-loop.patch +++ b/patches/server/Further-improve-server-tick-loop.patch @@ -90,7 +90,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper End // Spigot End - protected void runServer() { + public static volatile RuntimeException chunkSystemCrash; // Paper - rewrite chunk system @@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 5000L && this.nextTickTime - this.lastOverloadWarning >= 30000L) { // CraftBukkit long j = i / 50L; - - if (this.server.getWarnOnOverload()) // CraftBukkit -- MinecraftServer.LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", i, j); -+ MinecraftServer.LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", i, j); - this.nextTickTime += j * 50L; - this.lastOverloadWarning = this.nextTickTime; +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +From: Spottedleaf Date: Mon, 4 May 2020 10:06:24 -0700 Subject: [PATCH] Highly optimise single and multi-AABB VoxelShapes and collisions @@ -1497,8 +1497,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - return this.conditionallyFullOpaque; - } + protected boolean isTicking; + protected FluidState fluid; // Paper end + // Paper start + private long blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; @@ -1511,10 +1511,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 public void initCache() { this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() @@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - } this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here - this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light -- + this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light + + // Paper start + if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(this)) { + this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; diff --git a/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch b/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch deleted file mode 100644 index 86172f3554..0000000000 --- a/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch +++ /dev/null @@ -1,1254 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sat, 11 Apr 2020 03:56:07 -0400 -Subject: [PATCH] Implement Chunk Priority / Urgency System for Chunks - -Mark chunks that are blocking main thread for world generation as urgent - -Implements a general priority system so that chunks that are sorted in -the generator queues can prioritize certain chunks over another. - -Urgent chunks will jump to the front of the line, ensuring that a -sync chunk load on an ungenerated chunk does not lag the server for -a long period of time if the servers generator queues are filled with -lots of chunks already. - -This massively reduces the lag spikes from sync chunk gens. - -Then we further prioritize loading order so nearby chunks have higher -priority than distant chunks, reducing the pressure a high no tick -view distance holds on you. - -Chunks in front of the player have higher priority, to help with -fast traveling players keep up with their movement. - -diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java -+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java -@@ -0,0 +0,0 @@ public final class ChunkTaskManager { - } - - static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z) { -- dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1); -+ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 4); // Paper - 1->4 - } - - static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z, int indent, int maxDepth) { -@@ -0,0 +0,0 @@ public final class ChunkTaskManager { - PaperFileIOThread.LOGGER.error(indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getStatus().toString())); - PaperFileIOThread.LOGGER.error(indentStr + "Chunk Ticket Status - " + ChunkHolder.getStatus(chunkHolder.getTicketLevel())); - PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString())); -+ // Paper start -+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Holder Priority - " + chunkHolder.queueLevel); -+ -+ if (!chunkHolder.neighbors.isEmpty()) { -+ if (indent >= maxDepth) { -+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Neighbors: (Can't show, too deeply nested)"); -+ return; -+ } -+ PaperFileIOThread.LOGGER.error(indentStr + "Chunk Neighbors: "); -+ for (ChunkHolder neighbor : chunkHolder.neighbors.keySet()) { -+ ChunkStatus status = neighbor.getChunkHolderStatus(); -+ if (status != null && status.isOrAfter(ChunkHolder.getStatus(neighbor.getTicketLevel()))) { -+ continue; -+ } -+ int nx = neighbor.pos.x; -+ int nz = neighbor.pos.z; -+ if (seenChunks.contains(neighbor)) { -+ PaperFileIOThread.LOGGER.error(indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + " (CIRCULAR)"); -+ continue; -+ } -+ PaperFileIOThread.LOGGER.error(indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + ":"); -+ dumpChunkInfo(seenChunks, neighbor, nx, nz, indent + 1, maxDepth); -+ } -+ } -+ // Paper end - } - } - -diff --git a/src/main/java/net/minecraft/server/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 { - - static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); - -+ // Paper start - priority -+ private static int getPriorityBoost(final PrioritisedExecutor.Priority priority) { -+ if (priority.isLowerOrEqualPriority(PrioritisedExecutor.Priority.NORMAL)) { -+ return 0; -+ } -+ -+ int dist = PrioritisedExecutor.Priority.BLOCKING.ordinal() - PrioritisedExecutor.Priority.NORMAL.ordinal(); -+ -+ -+ return (net.minecraft.server.level.DistanceManager.URGENT_PRIORITY * (priority.ordinal() - PrioritisedExecutor.Priority.NORMAL.ordinal())) / dist; -+ } -+ // Paper end - priority -+ - private static long chunkLoadCounter = 0L; - public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, - final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { -@@ -0,0 +0,0 @@ public final class ChunkSystem { - final int minLevel = 33 + ChunkStatus.getDistance(toStatus); - final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; - final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -+ final int priorityBoost = getPriorityBoost(priority); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - -+ if (priorityBoost == net.minecraft.server.level.DistanceManager.URGENT_PRIORITY) { -+ level.chunkSource.markUrgent(chunkPos); -+ } else if (priorityBoost != 0) { -+ level.chunkSource.markHighPriority(chunkPos, priorityBoost); -+ } -+ - final Consumer loadCallback = (final ChunkAccess chunk) -> { - try { - if (onComplete != null) { -@@ -0,0 +0,0 @@ public final class ChunkSystem { - level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } -+ if (priorityBoost == net.minecraft.server.level.DistanceManager.URGENT_PRIORITY) { -+ level.chunkSource.clearUrgent(chunkPos); -+ } else if (priorityBoost != 0) { -+ level.chunkSource.clearPriorityTickets(chunkPos); -+ } - } - }; - -@@ -0,0 +0,0 @@ public final class ChunkSystem { - final int radius = toStatus.ordinal() - 1; - final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; - final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -+ final int priorityBoost = getPriorityBoost(priority); - - if (addTicket) { - level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } - level.chunkSource.runDistanceManagerUpdates(); - -+ if (priorityBoost != 0) { -+ level.chunkSource.markAreaHighPriority(chunkPos, priorityBoost, radius); -+ } -+ - final Consumer loadCallback = (final LevelChunk chunk) -> { - try { - if (onComplete != null) { -@@ -0,0 +0,0 @@ public final class ChunkSystem { - level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); - level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); - } -+ if (priorityBoost != 0) { -+ level.chunkSource.clearAreaPriorityTickets(chunkPos, radius); -+ } - } - }; - -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 @@ public final class MCUtil { - chunkData.addProperty("x", playerChunk.pos.x); - chunkData.addProperty("z", playerChunk.pos.z); - chunkData.addProperty("ticket-level", playerChunk.getTicketLevel()); -+ chunkData.addProperty("priority", playerChunk.queueLevel); // Paper - priority - chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString()); - chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey)); - chunkData.addProperty("status", status == null ? "unloaded" : status.toString()); -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index 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 final DebugBuffer chunkToSaveHistory; - public int oldTicketLevel; - private int ticketLevel; -- private int queueLevel; -+ public volatile int queueLevel; // Paper - private->public, make volatile since this is concurrently accessed - public final ChunkPos pos; - private boolean hasChangedSections; - private final ShortSet[] changedBlocksPerSection; -@@ -0,0 +0,0 @@ public class ChunkHolder { - private boolean resendLight; - private CompletableFuture pendingFullStateConfirmation; - -+ public ServerLevel getWorld() { return chunkMap.level; } // Paper - boolean isUpdateQueued = false; // Paper - private final ChunkMap chunkMap; // Paper - -@@ -0,0 +0,0 @@ public class ChunkHolder { - }); - } - -+ // Paper start -+ private boolean loadCallbackScheduled = false; -+ private boolean unloadCallbackScheduled = false; -+ // Paper end -+ - private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) { - this.pendingFullStateConfirmation.cancel(false); - playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); - } - - protected void updateFutures(ChunkMap chunkStorage, Executor executor) { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async ticket level update"); // Paper - ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel); - ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel); - boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; -@@ -0,0 +0,0 @@ public class ChunkHolder { - // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. - if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { - this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper - LevelChunk chunk = (LevelChunk)either.left().orElse(null); -- if (chunk != null) { -+ if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Paper - only invoke unload if load was called -+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... -+ if (ChunkHolder.this.unloadCallbackScheduled) { -+ return; -+ } -+ ChunkHolder.this.unloadCallbackScheduled = true; -+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... - chunkStorage.callbackExecutor.execute(() -> { -+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... -+ ChunkHolder.this.unloadCallbackScheduled = false; -+ if (ChunkHolder.this.ticketLevel <= 33) { -+ return; -+ } -+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... - // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick - // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. - // These actions may however happen deferred, so we manually set the needsSaving flag already here. -@@ -0,0 +0,0 @@ public class ChunkHolder { - this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); - // Paper start - cache ticking ready status - this.fullChunkFuture.thenAccept(either -> { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper - final Optional left = either.left(); - if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { - LevelChunk fullChunk = either.left().get(); - ChunkHolder.this.isFullChunkReady = true; - net.minecraft.server.ChunkSystem.onChunkBorder(fullChunk, this); -+ this.chunkMap.distanceManager.clearPriorityTickets(pos); // Paper - chunk priority - } - }); - this.updateChunkToSave(this.fullChunkFuture, "full"); -@@ -0,0 +0,0 @@ public class ChunkHolder { - this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); - // Paper start - cache ticking ready status - this.tickingChunkFuture.thenAccept(either -> { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper - either.ifLeft(chunk -> { - // note: Here is a very good place to add callbacks to logic waiting on this. - ChunkHolder.this.isTickingReady = true; -@@ -0,0 +0,0 @@ public class ChunkHolder { - this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); - // Paper start - cache ticking ready status - this.entityTickingChunkFuture.thenAccept(either -> { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper - either.ifLeft(chunk -> { - ChunkHolder.this.isEntityTickingReady = true; - net.minecraft.server.ChunkSystem.onChunkEntityTicking(chunk, this); -@@ -0,0 +0,0 @@ public class ChunkHolder { - this.demoteFullChunk(chunkStorage, playerchunk_state1); - } - -- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); -+ //this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); -+ // Paper start - raise IO/load priority if priority changes, use our preferred priority -+ priorityBoost = chunkMap.distanceManager.getChunkPriority(pos); -+ int currRequestedPriority = this.requestedPriority; -+ int priority = getDemandedPriority(); -+ int newRequestedPriority = this.requestedPriority = priority; -+ if (this.queueLevel > priority) { -+ int ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY; -+ if (priority <= 10) { -+ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; -+ } else if (priority <= 20) { -+ ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY; -+ } -+ chunkMap.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, ioPriority); -+ chunkMap.level.getChunkSource().getLightEngine().queue.changePriority(pos.toLong(), this.queueLevel, priority); // Paper // Restore this in chunk priority later? -+ } -+ if (currRequestedPriority != newRequestedPriority) { -+ this.onLevelChange.onLevelChange(this.pos, () -> this.queueLevel, priority, p -> this.queueLevel = p); // use preferred priority -+ int neighborsPriority = getNeighborsPriority(); -+ this.neighbors.forEach((neighbor, neighborDesired) -> neighbor.setNeighborPriority(this, neighborsPriority)); -+ } -+ // Paper end - this.oldTicketLevel = this.ticketLevel; - // CraftBukkit start - // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. - if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { - this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { -+ io.papermc.paper.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Paper - LevelChunk chunk = (LevelChunk)either.left().orElse(null); -- if (chunk != null) { -+ if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Paper - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33 -+ // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... -+ if (ChunkHolder.this.loadCallbackScheduled) { -+ return; -+ } -+ ChunkHolder.this.loadCallbackScheduled = true; -+ // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... - chunkStorage.callbackExecutor.execute(() -> { -- chunk.loadCallback(); -+ ChunkHolder.this.loadCallbackScheduled = false; // Paper - only schedule once, now the future is no longer completed as RIGHT if unloaded... -+ if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Paper " - }); - } - }).exceptionally((throwable) -> { -@@ -0,0 +0,0 @@ public class ChunkHolder { - }; - } - -- // Paper start -+ // Paper start - Chunk gen/load priority system -+ volatile int neighborPriority = -1; -+ volatile int priorityBoost = 0; -+ public final java.util.concurrent.ConcurrentHashMap neighbors = new java.util.concurrent.ConcurrentHashMap<>(); -+ public final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap neighborPriorities = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); -+ int requestedPriority = ChunkMap.MAX_CHUNK_DISTANCE + 1; // this priority is possible pending, but is used to ensure needless updates are not queued -+ -+ private int getDemandedPriority() { -+ int priority = neighborPriority; // if we have a neighbor priority, use it -+ int myPriority = getMyPriority(); -+ -+ if (priority == -1 || (ticketLevel <= 33 && priority > myPriority)) { -+ priority = myPriority; -+ } -+ -+ return Math.max(1, Math.min(Math.max(ticketLevel, ChunkMap.MAX_CHUNK_DISTANCE), priority)); -+ } -+ -+ private int getMyPriority() { -+ if (priorityBoost == DistanceManager.URGENT_PRIORITY) { -+ return 2; // Urgent - ticket level isn't always 31 so 33-30 = 3, but allow 1 more tasks to go below this for dependents -+ } -+ return ticketLevel - priorityBoost; -+ } -+ -+ private int getNeighborsPriority() { -+ return (neighborPriorities.isEmpty() ? getMyPriority() : getDemandedPriority()) + 1; -+ } -+ -+ public void onNeighborRequest(ChunkHolder neighbor, ChunkStatus status) { -+ neighbor.setNeighborPriority(this, getNeighborsPriority()); -+ this.neighbors.compute(neighbor, (playerChunk, currentWantedStatus) -> { -+ if (currentWantedStatus == null || !currentWantedStatus.isOrAfter(status)) { -+ //System.out.println(this + " request " + neighbor + " at " + status + " currently " + currentWantedStatus); -+ return status; -+ } else { -+ //System.out.println(this + " requested " + neighbor + " at " + status + " but thats lower than other wanted status " + currentWantedStatus); -+ return currentWantedStatus; -+ } -+ }); -+ -+ } -+ -+ public void onNeighborDone(ChunkHolder neighbor, ChunkStatus chunkstatus, ChunkAccess chunk) { -+ this.neighbors.compute(neighbor, (playerChunk, wantedStatus) -> { -+ if (wantedStatus != null && chunkstatus.isOrAfter(wantedStatus)) { -+ //System.out.println(this + " neighbor done at " + neighbor + " for status " + chunkstatus + " wanted " + wantedStatus); -+ neighbor.removeNeighborPriority(this); -+ return null; -+ } else { -+ //System.out.println(this + " neighbor finished our previous request at " + neighbor + " for status " + chunkstatus + " but we now want instead " + wantedStatus); -+ return wantedStatus; -+ } -+ }); -+ } -+ -+ private void removeNeighborPriority(ChunkHolder requester) { -+ synchronized (neighborPriorities) { -+ neighborPriorities.remove(requester.pos.toLong()); -+ recalcNeighborPriority(); -+ } -+ checkPriority(); -+ } -+ -+ -+ private void setNeighborPriority(ChunkHolder requester, int priority) { -+ synchronized (neighborPriorities) { -+ if (!Integer.valueOf(priority).equals(neighborPriorities.put(requester.pos.toLong(), Integer.valueOf(priority)))) { -+ recalcNeighborPriority(); -+ } -+ } -+ checkPriority(); -+ } -+ -+ private void recalcNeighborPriority() { -+ neighborPriority = -1; -+ if (!neighborPriorities.isEmpty()) { -+ synchronized (neighborPriorities) { -+ for (Integer neighbor : neighborPriorities.values()) { -+ if (neighbor < neighborPriority || neighborPriority == -1) { -+ neighborPriority = neighbor; -+ } -+ } -+ } -+ } -+ } -+ private void checkPriority() { -+ if (this.requestedPriority != getDemandedPriority()) this.chunkMap.queueHolderUpdate(this); -+ } -+ -+ public final double getDistance(ServerPlayer player) { -+ return getDistance(player.getX(), player.getZ()); -+ } -+ public final double getDistance(double blockX, double blockZ) { -+ int cx = net.minecraft.server.MCUtil.fastFloor(blockX) >> 4; -+ int cz = net.minecraft.server.MCUtil.fastFloor(blockZ) >> 4; -+ final double x = pos.x - cx; -+ final double z = pos.z - cz; -+ return (x * x) + (z * z); -+ } -+ -+ public final double getDistanceFrom(BlockPos pos) { -+ return getDistance(pos.getX(), pos.getZ()); -+ } -+ -+ public static ChunkStatus getNextStatus(ChunkStatus status) { -+ if (status == ChunkStatus.FULL) { -+ return status; -+ } -+ return CHUNK_STATUSES.get(status.getIndex() + 1); -+ } -+ public CompletableFuture> getStatusFutureUncheckedMain(ChunkStatus chunkstatus) { -+ return ensureMain(getFutureIfPresentUnchecked(chunkstatus)); -+ } -+ public CompletableFuture ensureMain(CompletableFuture future) { -+ return future.thenApplyAsync(r -> r, chunkMap.mainInvokingExecutor); -+ } -+ -+ @Override -+ public String toString() { -+ return "PlayerChunk{" + -+ "location=" + pos + -+ ", ticketLevel=" + ticketLevel + "/" + getStatus(this.ticketLevel) + -+ ", chunkHolderStatus=" + getChunkHolderStatus() + -+ ", neighborPriority=" + getNeighborsPriority() + -+ ", priority=(" + ticketLevel + " - " + priorityBoost +" vs N " + neighborPriority + ") = " + getDemandedPriority() + " A " + queueLevel + -+ '}'; -+ } - public final boolean isEntityTickingReady() { - return this.isEntityTickingReady; - } -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 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 - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop mainThreadExecutor; -+ final java.util.concurrent.Executor mainInvokingExecutor; // Paper - public ChunkGenerator generator; - private RandomState randomState; - public final Supplier overworldDataStorage; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - this.mainThreadExecutor = mainThreadExecutor; -+ // Paper start -+ this.mainInvokingExecutor = (run) -> { -+ if (MCUtil.isMainThread()) { -+ run.run(); -+ } else { -+ mainThreadExecutor.execute(run); -+ } -+ }; -+ // Paper end - ProcessorMailbox threadedmailbox = ProcessorMailbox.create(executor, "worldgen"); - - Objects.requireNonNull(mainThreadExecutor); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - }); - } - -+ // Paper start - Chunk Prioritization -+ public void queueHolderUpdate(ChunkHolder playerchunk) { -+ Runnable runnable = () -> { -+ if (isUnloading(playerchunk)) { -+ return; // unloaded -+ } -+ distanceManager.pendingChunkUpdates.add(playerchunk); -+ if (!distanceManager.pollingPendingChunkUpdates) { -+ level.getChunkSource().runDistanceManagerUpdates(); -+ } -+ }; -+ if (MCUtil.isMainThread()) { -+ // We can't use executor here because it will not execute tasks if its currently in the middle of executing tasks... -+ runnable.run(); -+ } else { -+ mainThreadExecutor.execute(runnable); -+ } -+ } -+ -+ private boolean isUnloading(ChunkHolder playerchunk) { -+ return playerchunk == null || toDrop.contains(playerchunk.pos.toLong()); -+ } -+ -+ private void updateChunkPriorityMap(it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap map, long chunk, int level) { -+ int prev = map.getOrDefault(chunk, -1); -+ if (level > prev) { -+ map.put(chunk, level); -+ } -+ } -+ // Paper end -+ - private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { - double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); - double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - List list1 = new ArrayList(); - int j = centerChunk.x; - int k = centerChunk.z; -+ ChunkHolder requestingNeighbor = getUpdatingChunkIfPresent(centerChunk.toLong()); // Paper - - for (int l = -margin; l <= margin; ++l) { - for (int i1 = -margin; i1 <= margin; ++i1) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(j1); - CompletableFuture> completablefuture = playerchunk.getOrScheduleFuture(chunkstatus, this); -+ // Paper start -+ if (requestingNeighbor != null && requestingNeighbor != playerchunk && !completablefuture.isDone()) { -+ requestingNeighbor.onNeighborRequest(playerchunk, chunkstatus); -+ completablefuture.thenAccept(either -> { -+ requestingNeighbor.onNeighborDone(playerchunk, chunkstatus, either.left().orElse(null)); -+ }); -+ } -+ // Paper end - - list1.add(playerchunk); - list.add(completablefuture); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - if (requiredStatus == ChunkStatus.EMPTY) { - return this.scheduleChunkLoad(chunkcoordintpair); - } else { -+ // Paper start - revert 1.17 chunk system changes -+ CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this); -+ return future.thenComposeAsync((either) -> { -+ Optional optional = either.left(); -+ if (!optional.isPresent()) { -+ return CompletableFuture.completedFuture(either); -+ } -+ // Paper end - revert 1.17 chunk system changes - if (requiredStatus == ChunkStatus.LIGHT) { - this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair); - } - -- Optional optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); -+ // Paper - revert 1.17 chunk system changes - - if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) { - CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (ichunkaccess) -> { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } else { - return this.scheduleChunkGeneration(holder, requiredStatus); - } -+ }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Paper - revert 1.17 chunk system changes - } - } - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - }; - - CompletableFuture chunkSaveFuture = this.level.asyncChunkTaskManager.getChunkSaveFuture(pos.x, pos.z); -+ // Paper start -+ ChunkHolder playerChunk = getUpdatingChunkIfPresent(pos.toLong()); -+ int chunkPriority = playerChunk != null ? playerChunk.requestedPriority : 33; -+ int priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY; -+ -+ if (chunkPriority <= 10) { -+ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; -+ } else if (chunkPriority <= 20) { -+ priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY; -+ } -+ boolean isHighestPriority = priority == com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; -+ // Paper end - if (chunkSaveFuture != null) { -- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, -- com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture); -- this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY); -+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority, chunkSaveFuture); // Paper - } else { -- this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, -- com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false); -+ this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority); // Paper - } -+ this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, priority); // Paper - return ret; - // Paper end - Async chunk io - } -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.releaseLightTicket(chunkcoordintpair); - return CompletableFuture.completedFuture(Either.right(playerchunk_failure)); - }); -- }, executor); -+ }, executor).thenComposeAsync((either) -> { // Paper start - force competion on the main thread -+ return CompletableFuture.completedFuture(either); -+ }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute -+ // Paper end - force competion on the main thread - } - - protected void releaseLightTicket(ChunkPos pos) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - long i = chunkHolder.getPos().toLong(); - - Objects.requireNonNull(chunkHolder); -- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, chunkHolder::getTicketLevel)); -+ mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, () -> 1)); // Paper - final loads are always urgent! - }); - } - -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index 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 static int getTicketLevelAt(SortedArraySet> tickets) { -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getTicketLevelAt"); // Paper - return !tickets.isEmpty() ? ((Ticket) tickets.first()).getTicketLevel() : ChunkMap.MAX_CHUNK_DISTANCE + 1; - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - public boolean runAllUpdates(ChunkMap chunkStorage) { - this.naturalSpawnChunkCounter.runAllUpdates(); - this.tickingTicketsTracker.runAllUpdates(); -+ org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper - this.playerTicketManager.runAllUpdates(); - int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); - boolean flag = i != 0; -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - - // Paper start - if (!this.pendingChunkUpdates.isEmpty()) { -+ this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority - while(!this.pendingChunkUpdates.isEmpty()) { - ChunkHolder remove = this.pendingChunkUpdates.remove(); - remove.isUpdateQueued = false; - remove.updateFutures(chunkStorage, this.mainThreadExecutor); - } -+ } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority - // Paper end - return true; - } else { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - return flag; - } - } -+ boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority - - boolean addTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper - SortedArraySet> arraysetsorted = this.getTickets(i); - int j = DistanceManager.getTicketLevelAt(arraysetsorted); - Ticket ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - boolean removeTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper - SortedArraySet> arraysetsorted = this.getTickets(i); -+ int oldLevel = getTicketLevelAt(arraysetsorted); // Paper - - boolean removed = false; // CraftBukkit - if (arraysetsorted.remove(ticket)) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - this.tickets.remove(i); - } - -- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); -+ // Paper start - Chunk priority -+ int newLevel = getTicketLevelAt(arraysetsorted); -+ if (newLevel > oldLevel) { -+ this.ticketTracker.update(i, newLevel, false); -+ } -+ // Paper end - return removed; // CraftBukkit - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - }); - } - -+ // Paper start - Chunk priority -+ public static final int PRIORITY_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; -+ public static final int URGENT_PRIORITY = 29; -+ public boolean delayDistanceManagerTick = false; -+ public boolean markUrgent(ChunkPos coords) { -+ return addPriorityTicket(coords, TicketType.URGENT, URGENT_PRIORITY); -+ } -+ public boolean markHighPriority(ChunkPos coords, int priority) { -+ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority)); -+ return addPriorityTicket(coords, TicketType.PRIORITY, priority); -+ } -+ -+ public void markAreaHighPriority(ChunkPos center, int priority, int radius) { -+ delayDistanceManagerTick = true; -+ priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority)); -+ int finalPriority = priority; -+ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> { -+ addPriorityTicket(coords, TicketType.PRIORITY, finalPriority); -+ }); -+ delayDistanceManagerTick = false; -+ chunkMap.level.getChunkSource().runDistanceManagerUpdates(); -+ } -+ -+ public void clearAreaPriorityTickets(ChunkPos center, int radius) { -+ delayDistanceManagerTick = true; -+ net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> { -+ this.removeTicket(coords.toLong(), new Ticket(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords)); -+ }); -+ delayDistanceManagerTick = false; -+ chunkMap.level.getChunkSource().runDistanceManagerUpdates(); -+ } -+ -+ private boolean addPriorityTicket(ChunkPos coords, TicketType ticketType, int priority) { -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket"); -+ long pair = coords.toLong(); -+ ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair); -+ if ((chunk != null && chunk.isFullChunkReady())) { -+ return false; -+ } -+ -+ boolean success; -+ if (!(success = updatePriorityTicket(coords, ticketType, priority))) { -+ Ticket ticket = new Ticket(ticketType, PRIORITY_TICKET_LEVEL, coords); -+ ticket.priority = priority; -+ success = this.addTicket(pair, ticket); -+ } else { -+ if (chunk == null) { -+ chunk = chunkMap.getUpdatingChunkIfPresent(pair); -+ } -+ chunkMap.queueHolderUpdate(chunk); -+ } -+ -+ //chunkMap.world.getWorld().spawnParticle(priority <= 15 ? org.bukkit.Particle.EXPLOSION_HUGE : org.bukkit.Particle.EXPLOSION_NORMAL, chunkMap.world.getWorld().getPlayers(), null, coords.x << 4, 70, coords.z << 4, 2, 0, 0, 0, 1, null, true); -+ -+ chunkMap.level.getChunkSource().runDistanceManagerUpdates(); -+ -+ return success; -+ } -+ -+ private boolean updatePriorityTicket(ChunkPos coords, TicketType type, int priority) { -+ SortedArraySet> tickets = this.tickets.get(coords.toLong()); -+ if (tickets == null) { -+ return false; -+ } -+ for (Ticket ticket : tickets) { -+ if (ticket.getType() == type) { -+ // We only support increasing, not decreasing, too complicated -+ ticket.setCreatedTick(this.ticketTickCounter); -+ ticket.priority = Math.max(ticket.priority, priority); -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ -+ public int getChunkPriority(ChunkPos coords) { -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getChunkPriority"); -+ SortedArraySet> tickets = this.tickets.get(coords.toLong()); -+ if (tickets == null) { -+ return 0; -+ } -+ for (Ticket ticket : tickets) { -+ if (ticket.getType() == TicketType.URGENT) { -+ return URGENT_PRIORITY; -+ } -+ } -+ for (Ticket ticket : tickets) { -+ if (ticket.getType() == TicketType.PRIORITY && ticket.priority > 0) { -+ return ticket.priority; -+ } -+ } -+ return 0; -+ } -+ -+ public void clearPriorityTickets(ChunkPos coords) { -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearPriority"); -+ this.removeTicket(coords.toLong(), new Ticket(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords)); -+ } -+ -+ public void clearUrgent(ChunkPos coords) { -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearUrgent"); -+ this.removeTicket(coords.toLong(), new Ticket(TicketType.URGENT, PRIORITY_TICKET_LEVEL, coords)); -+ } -+ // Paper end -+ - protected void updateChunkForced(ChunkPos pos, boolean forced) { - Ticket ticket = new Ticket<>(TicketType.FORCED, 31, pos); - long i = pos.toLong(); -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 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; - } -+ -+ public boolean markUrgent(ChunkPos coords) { -+ return this.distanceManager.markUrgent(coords); -+ } -+ -+ public boolean markHighPriority(ChunkPos coords, int priority) { -+ return this.distanceManager.markHighPriority(coords, priority); -+ } -+ -+ public void markAreaHighPriority(ChunkPos center, int priority, int radius) { -+ this.distanceManager.markAreaHighPriority(center, priority, radius); -+ } -+ -+ public void clearAreaPriorityTickets(ChunkPos center, int radius) { -+ this.distanceManager.clearAreaPriorityTickets(center, radius); -+ } -+ -+ public void clearPriorityTickets(ChunkPos coords) { -+ this.distanceManager.clearPriorityTickets(coords); -+ } -+ -+ public void clearUrgent(ChunkPos coords) { -+ this.distanceManager.clearUrgent(coords); -+ } - // Paper end - async chunk io - - @Nullable -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - Objects.requireNonNull(completablefuture); - if (!completablefuture.isDone()) { // Paper - // Paper start - async chunk io/loading -+ ChunkPos pair = new ChunkPos(x1, z1); // Paper - Chunk priority -+ this.distanceManager.markUrgent(pair); // Paper - Chunk priority - this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); - com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1); - // Paper end -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - chunkproviderserver_b.managedBlock(completablefuture::isDone); - com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug - this.level.timings.syncChunkLoad.stopTiming(); // Paper -+ this.distanceManager.clearPriorityTickets(pair); // Paper - Chunk priority -+ this.distanceManager.clearUrgent(pair); // Paper - Chunk priority - } // Paper - ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { - return ichunkaccess1; -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - if (create && !currentlyUnloading) { - // CraftBukkit end - this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); -+ if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority - if (this.chunkAbsent(playerchunk, l)) { - ProfilerFiller gameprofilerfiller = this.level.getProfiler(); - - gameprofilerfiller.push("chunkLoad"); -+ distanceManager.delayDistanceManagerTick = false; // Paper - Chunk priority - ensure this is never false - this.runDistanceManagerUpdates(); - playerchunk = this.getVisibleChunkIfPresent(k); - gameprofilerfiller.pop(); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - } - } - -- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); -+ // Paper start - Chunk priority -+ CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); -+ if (isUrgent) { -+ future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); -+ } -+ return future; -+ // Paper end - } - - private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - } - - public boolean runDistanceManagerUpdates() { -+ if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority - boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); - boolean flag1 = this.chunkMap.promoteChunkMap(); - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- 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 { - private int lastRecordedArmor = Integer.MIN_VALUE; - private int lastRecordedLevel = Integer.MIN_VALUE; - private int lastRecordedExperience = Integer.MIN_VALUE; -+ public boolean isRealPlayer; // Paper - chunk priority - private float lastSentHealth = -1.0E8F; - private int lastSentFood = -99999999; - private boolean lastFoodSaturationZero = true; -@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { - this.bukkitPickUpLoot = true; - this.maxHealthCache = this.getMaxHealth(); - } -+ // Paper start - Chunk priority -+ public BlockPos getPointInFront(double inFront) { -+ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason -+ final double x = getX() + inFront * Math.cos(rads); -+ final double z = getZ() + inFront * Math.sin(rads); -+ return new BlockPos(x, getY(), z); -+ } -+ -+ public ChunkPos getChunkInFront(double inFront) { -+ double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason -+ final double x = getX() + (inFront * 16) * Math.cos(rads); -+ final double z = getZ() + (inFront * 16) * Math.sin(rads); -+ return new ChunkPos(Mth.floor(x) >> 4, Mth.floor(z) >> 4); -+ } -+ // Paper end - - // Yes, this doesn't match Vanilla, but it's the best we can do for now. - // If this is an issue, PRs are welcome -diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -@@ -0,0 +0,0 @@ import org.slf4j.Logger; - public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { - private static final Logger LOGGER = LogUtils.getLogger(); - private final ProcessorMailbox taskMailbox; -- private final ObjectList> lightTasks = new ObjectArrayList<>(); -- private final ChunkMap chunkMap; -+ // Paper start -+ private static final int MAX_PRIORITIES = ChunkMap.MAX_CHUNK_DISTANCE + 2; -+ -+ static class ChunkLightQueue { -+ public boolean shouldFastUpdate; -+ java.util.ArrayDeque pre = new java.util.ArrayDeque(); -+ java.util.ArrayDeque post = new java.util.ArrayDeque(); -+ -+ ChunkLightQueue(long chunk) {} -+ } -+ -+ static class PendingLightTask { -+ long chunkId; -+ IntSupplier priority; -+ Runnable pre; -+ Runnable post; -+ boolean fastUpdate; -+ -+ public PendingLightTask(long chunkId, IntSupplier priority, Runnable pre, Runnable post, boolean fastUpdate) { -+ this.chunkId = chunkId; -+ this.priority = priority; -+ this.pre = pre; -+ this.post = post; -+ this.fastUpdate = fastUpdate; -+ } -+ } -+ -+ -+ // Retain the chunks priority level for queued light tasks -+ class LightQueue { -+ private int size = 0; -+ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap[] buckets = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap[MAX_PRIORITIES]; -+ private final java.util.concurrent.ConcurrentLinkedQueue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ private final java.util.concurrent.ConcurrentLinkedQueue priorityChanges = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ -+ private LightQueue() { -+ for (int i = 0; i < buckets.length; i++) { -+ buckets[i] = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>(); -+ } -+ } -+ -+ public void changePriority(long pair, int currentPriority, int priority) { -+ this.priorityChanges.add(() -> { -+ ChunkLightQueue remove = this.buckets[currentPriority].remove(pair); -+ if (remove != null) { -+ ChunkLightQueue existing = this.buckets[Math.max(1, priority)].put(pair, remove); -+ if (existing != null) { -+ remove.pre.addAll(existing.pre); -+ remove.post.addAll(existing.post); -+ } -+ } -+ }); -+ } -+ -+ public final void addChunk(long chunkId, IntSupplier priority, Runnable pre, Runnable post) { -+ pendingTasks.add(new PendingLightTask(chunkId, priority, pre, post, true)); -+ tryScheduleUpdate(); -+ } -+ -+ public final void add(long chunkId, IntSupplier priority, ThreadedLevelLightEngine.TaskType type, Runnable run) { -+ pendingTasks.add(new PendingLightTask(chunkId, priority, type == TaskType.PRE_UPDATE ? run : null, type == TaskType.POST_UPDATE ? run : null, false)); -+ } -+ public final void add(PendingLightTask update) { -+ int priority = update.priority.getAsInt(); -+ ChunkLightQueue lightQueue = this.buckets[priority].computeIfAbsent(update.chunkId, ChunkLightQueue::new); -+ -+ if (update.pre != null) { -+ this.size++; -+ lightQueue.pre.add(update.pre); -+ } -+ if (update.post != null) { -+ this.size++; -+ lightQueue.post.add(update.post); -+ } -+ if (update.fastUpdate) { -+ lightQueue.shouldFastUpdate = true; -+ } -+ } -+ -+ public final boolean isEmpty() { -+ return this.size == 0 && this.pendingTasks.isEmpty(); -+ } -+ -+ public final int size() { -+ return this.size; -+ } -+ -+ public boolean poll(java.util.List pre, java.util.List post) { -+ PendingLightTask pending; -+ while ((pending = pendingTasks.poll()) != null) { -+ add(pending); -+ } -+ Runnable run; -+ while ((run = priorityChanges.poll()) != null) { -+ run.run(); -+ } -+ boolean hasWork = false; -+ it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap[] buckets = this.buckets; -+ int priority = 0; -+ while (priority < MAX_PRIORITIES && !isEmpty()) { -+ it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap bucket = buckets[priority]; -+ if (bucket.isEmpty()) { -+ priority++; -+ if (hasWork) { -+ return true; -+ } else { -+ continue; -+ } -+ } -+ ChunkLightQueue queue = bucket.removeFirst(); -+ this.size -= queue.pre.size() + queue.post.size(); -+ pre.addAll(queue.pre); -+ post.addAll(queue.post); -+ queue.pre.clear(); -+ queue.post.clear(); -+ hasWork = true; -+ if (queue.shouldFastUpdate) { -+ return true; -+ } -+ } -+ return hasWork; -+ } -+ } -+ -+ final LightQueue queue = new LightQueue(); -+ // Paper end -+ private final ChunkMap chunkMap; private final ChunkMap playerChunkMap; // Paper - private final ProcessorHandle> sorterMailbox; - private volatile int taskPerBatch = 5; - private final AtomicBoolean scheduled = new AtomicBoolean(); - - public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { - super(chunkProvider, true, hasBlockLight); -- this.chunkMap = chunkStorage; -+ this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper - this.sorterMailbox = executor; - this.taskMailbox = processor; - } -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { -- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> { -- this.lightTasks.add(Pair.of(stage, task)); -- if (this.lightTasks.size() >= this.taskPerBatch) { -- this.runUpdate(); -- } -- -- }, ChunkPos.asLong(x, z), completedLevelSupplier)); -+ // Paper start - replace method -+ this.queue.add(ChunkPos.asLong(x, z), completedLevelSupplier, stage, task); -+ // Paper end - } - - @Override -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { - ChunkPos chunkPos = chunk.getPos(); -- chunk.setLightCorrect(false); -- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -+ // Paper start -+ //ichunkaccess.b(false); // Don't need to disable this -+ long pair = chunkPos.toLong(); -+ CompletableFuture future = new CompletableFuture<>(); -+ IntSupplier prioritySupplier = playerChunkMap.getChunkQueueLevel(pair); -+ boolean[] skippedPre = {false}; -+ this.queue.addChunk(pair, prioritySupplier, Util.name(() -> { -+ // Paper end - LevelChunkSection[] levelChunkSections = chunk.getSections(); - - for(int i = 0; i < chunk.getSectionsCount(); ++i) { -@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - }, () -> { - return "lightChunk " + chunkPos + " " + excludeBlocks; -- })); -- return CompletableFuture.supplyAsync(() -> { -+ // Paper start - merge the 2 together -+ }), () -> { -+ this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved from below, we want to call this even when returning early -+ if (skippedPre[0]) return; // Paper - future's already complete - chunk.setLightCorrect(true); - super.retainData(chunkPos, false); -- this.chunkMap.releaseLightTicket(chunkPos); -- return chunk; -- }, (runnable) -> { -- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable); -+ //this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved up -+ future.complete(chunk); - }); -+ return future; -+ // Paper end - } - - public void tryScheduleUpdate() { -- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { -+ if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper - this.taskMailbox.tell(() -> { - this.runUpdate(); - this.scheduled.set(false); -+ tryScheduleUpdate(); // Paper - if we still have work to do, do it! - }); - } - - } - -+ // Paper start - replace impl -+ private final java.util.List pre = new java.util.ArrayList<>(); -+ private final java.util.List post = new java.util.ArrayList<>(); - private void runUpdate() { -- int i = Math.min(this.lightTasks.size(), this.taskPerBatch); -- ObjectListIterator> objectListIterator = this.lightTasks.iterator(); -- -- int j; -- for(j = 0; objectListIterator.hasNext() && j < i; ++j) { -- Pair pair = objectListIterator.next(); -- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { -- pair.getSecond().run(); -- } -+ if (queue.poll(pre, post)) { -+ pre.forEach(Runnable::run); -+ pre.clear(); -+ super.runUpdates(Integer.MAX_VALUE, true, true); -+ post.forEach(Runnable::run); -+ post.clear(); -+ } else { -+ // might have level updates to go still -+ super.runUpdates(Integer.MAX_VALUE, true, true); - } -- -- objectListIterator.back(j); -- super.runUpdates(Integer.MAX_VALUE, true, true); -- -- for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) { -- Pair pair2 = objectListIterator.next(); -- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { -- pair2.getSecond().run(); -- } -- -- objectListIterator.remove(); -- } -- -+ // Paper end - } - - public void setTaskPerBatch(int taskBatchSize) { -diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java -index 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> { - public final T key; - public long createdTick; - public long delayUnloadBy; // Paper -+ public int priority; // Paper - Chunk priority - - protected Ticket(TicketType type, int level, T argument) { - this.type = type; -diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java -index 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 -+ public static final TicketType PRIORITY = create("priority", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper -+ public static final TicketType URGENT = create("urgent", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper - - private final String name; - private final Comparator comparator; -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -0,0 +0,0 @@ public abstract class PlayerList { - } - - public void placeNewPlayer(Connection connection, ServerPlayer player) { -+ player.isRealPlayer = true; // Paper - Chunk priority - GameProfile gameprofile = player.getGameProfile(); - GameProfileCache usercache = this.server.getProfileCache(); - Optional optional = usercache.get(gameprofile.getId()); -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - private BlockPos blockPosition; - private ChunkPos chunkPosition; - private Vec3 deltaMovement; -- private float yRot; -+ public float yRot; // Paper - private->public - private float xRot; - public float yRotO; - public float xRotO; -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - return NEIGHBOUR_CACHE_RADIUS; - } - -- boolean loadedTicketLevel; -+ boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Paper - public accessor - private long neighbourChunksLoadedBitset; - private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; - -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - - // CraftBukkit start - public void loadCallback() { -+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper - // Paper start - neighbour cache - int chunkX = this.chunkPos.x; - int chunkZ = this.chunkPos.z; -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - } - - public void unloadCallback() { -+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper - org.bukkit.Server server = this.level.getCraftServer(); - org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved()); - server.getPluginManager().callEvent(unloadEvent); diff --git a/patches/server/Implement-Player-Client-Options-API.patch b/patches/server/Implement-Player-Client-Options-API.patch index bfdf48766a..cf9b660a10 100644 --- a/patches/server/Implement-Player-Client-Options-API.patch +++ b/patches/server/Implement-Player-Client-Options-API.patch @@ -101,8 +101,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - public void setSendViewDistance(int viewDistance) { - throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO + connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message); + } } + + @Override diff --git a/patches/server/Improve-Chunk-Status-Transition-Speed.patch b/patches/server/Improve-Chunk-Status-Transition-Speed.patch deleted file mode 100644 index e8fdb7f1c6..0000000000 --- a/patches/server/Improve-Chunk-Status-Transition-Speed.patch +++ /dev/null @@ -1,81 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Fri, 29 May 2020 23:32:14 -0400 -Subject: [PATCH] Improve Chunk Status Transition Speed - -When a chunk is loaded from disk that has already been generated, -the server has to promote the chunk through the system to reach -it's current desired status level. - -This results in every single status transition going from the main thread -to the world gen threads, only to discover it has no work it actually -needs to do.... and then it returns back to main. - -This back and forth costs a lot of time and can really delay chunk loads -when the server is under high TPS due to their being a lot of time in -between chunk load times, as well as hogs up the chunk threads from doing -actual generation and light work. - -Additionally, the whole task system uses a lot of CPU on the server threads anyways. - -So by optimizing status transitions for status's that are already complete, -we can run them to the desired level while on main thread (where it has -to happen anyways) instead of ever jumping to world gen thread. - -This will improve chunk loading effeciency to be reduced down to the following -scenario / path: - -1) MAIN: Chunk Requested, Load Request sent to ChunkTaskManager / IO Queue -2) IO: Once position in queue comes, submit read IO data and schedule to chunk task thread -3) CHUNK: Once IO is loaded and position in queue comes, deserialize the chunk data, process conversions, submit to main queue -4) MAIN: next Chunk Task process (Mid Tick or End Of Tick), load chunk data into world (POI, main thread tasks) -5) MAIN: process status transitions all the way to LIGHT, light schedules Threaded task -6) SERVER: Light tasks register light enablement for chunk and any lighting needing to be done -7) MAIN: Task returns to main, finish processing to FULL/TICKING status - -Previously would have hopped to SERVER around 12+ times there extra. - -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 { - // Paper end - optimise anyPlayerCloseEnoughForSpawning - long lastAutoSaveTime; // Paper - incremental autosave - long inactiveTimeStart; // Paper - incremental autosave -+ // Paper start - optimize chunk status progression without jumping through thread pool -+ public boolean canAdvanceStatus() { -+ ChunkStatus status = getChunkHolderStatus(); -+ ChunkAccess chunk = getAvailableChunkNow(); -+ return chunk != null && (status == null || chunk.getStatus().isOrAfter(getNextStatus(status))); -+ } -+ // Paper end - - public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { - this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); -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 either.mapLeft((list) -> { - return (LevelChunk) list.get(list.size() / 2); - }); -- }, this.mainThreadExecutor); -+ }, this.mainInvokingExecutor); // Paper - } - - @Nullable -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - return "chunkGenerate " + requiredStatus.getName(); - }); - Executor executor = (runnable) -> { -+ // Paper start - optimize chunk status progression without jumping through thread pool -+ if (holder.canAdvanceStatus()) { -+ this.mainInvokingExecutor.execute(runnable); -+ return; -+ } -+ // Paper end - this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); - }; - diff --git a/patches/server/Improve-boat-collision-performance.patch b/patches/server/Improve-boat-collision-performance.patch index 8454d84a27..4b94c67a01 100644 --- a/patches/server/Improve-boat-collision-performance.patch +++ b/patches/server/Improve-boat-collision-performance.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 2 Aug 2021 10:10:40 +0200 Subject: [PATCH] Improve boat collision performance diff --git a/patches/server/Improve-inlinig-for-some-hot-IBlockData-methods.patch b/patches/server/Improve-inlinig-for-some-hot-IBlockData-methods.patch index e8f8910af0..d36dfee581 100644 --- a/patches/server/Improve-inlinig-for-some-hot-IBlockData-methods.patch +++ b/patches/server/Improve-inlinig-for-some-hot-IBlockData-methods.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 6 Jul 2020 20:46:50 -0700 Subject: [PATCH] Improve inlinig for some hot IBlockData methods @@ -9,9 +9,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - return this.shapeExceedsCube; + return this.conditionallyFullOpaque; } - // Paper end + // Paper end - starlight + // Paper start + protected boolean isTicking; + protected FluidState fluid; diff --git a/patches/server/Improved-Watchdog-Support.patch b/patches/server/Improved-Watchdog-Support.patch index 87a10c2b59..792fa9eb92 100644 --- a/patches/server/Improved-Watchdog-Support.patch +++ b/patches/server/Improved-Watchdog-Support.patch @@ -92,7 +92,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public static S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); - Thread thread = new Thread(() -> { + Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system @@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - CompletableFuture completablefuture; diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java @@ -329,19 +317,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 // Paper start - Prevent tile entity and entity crashes final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -0,0 +0,0 @@ public final class CraftServer implements Server { - - @Override - public boolean isPrimaryThread() { -- return Thread.currentThread().equals(console.serverThread); // Paper - Fix issues with detecting main thread properly -+ return Thread.currentThread().equals(console.serverThread) || Thread.currentThread().equals(net.minecraft.server.MinecraftServer.getServer().shutdownThread); // Paper - Fix issues with detecting main thread properly, the only time Watchdog will be used is during a crash shutdown which is a "try our best" scenario - } - - // Paper start diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java @@ -509,14 +484,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/org/spigotmc/WatchdogThread.java +++ b/src/main/java/org/spigotmc/WatchdogThread.java @@ -0,0 +0,0 @@ import org.bukkit.Bukkit; - public class WatchdogThread extends Thread + public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system { + public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper private static WatchdogThread instance; private long timeoutTime; private boolean restart; -@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread +@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa { if ( WatchdogThread.instance == null ) { @@ -524,7 +499,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); WatchdogThread.instance.start(); } else -@@ -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 start Logger log = Bukkit.getServer().getLogger(); long currentTime = WatchdogThread.monotonicMillis(); @@ -541,16 +516,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 lastEarlyWarning = currentTime; if (isLongTimeout) { // Paper end -@@ -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 Paper!):" ); // Paper - com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper -- WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); -+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log ); - log.log( Level.SEVERE, "------------------------------" ); - // - // Paper start - Only print full dump on long timeouts -@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread +@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa if ( isLongTimeout ) { diff --git a/patches/server/Load-Chunks-for-Login-Asynchronously.patch b/patches/server/Load-Chunks-for-Login-Asynchronously.patch index c7fcd376b1..926fc6e3f3 100644 --- a/patches/server/Load-Chunks-for-Login-Asynchronously.patch +++ b/patches/server/Load-Chunks-for-Login-Asynchronously.patch @@ -116,7 +116,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 // private final Map advancements; @@ -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 + // Paper start + ServerPlayer prev = pendingPlayers.put(player.getUUID(), player); diff --git a/patches/server/Log-when-the-async-catcher-is-tripped.patch b/patches/server/Log-when-the-async-catcher-is-tripped.patch index 1933accd2a..ee72edf1fa 100644 --- a/patches/server/Log-when-the-async-catcher-is-tripped.patch +++ b/patches/server/Log-when-the-async-catcher-is-tripped.patch @@ -12,7 +12,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +++ b/src/main/java/org/spigotmc/AsyncCatcher.java @@ -0,0 +0,0 @@ public class AsyncCatcher { - if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper + if ( !io.papermc.paper.util.TickThread.isTickThread() ) // Paper // Paper - rewrite chunk system { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper throw new IllegalStateException( "Asynchronous " + reason + "!" ); diff --git a/patches/server/MC-4-Fix-item-position-desync.patch b/patches/server/MC-4-Fix-item-position-desync.patch index 46ee5cb911..bc9d0fa294 100644 --- a/patches/server/MC-4-Fix-item-position-desync.patch +++ b/patches/server/MC-4-Fix-item-position-desync.patch @@ -31,9 +31,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - } - public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) { - // Paper end + return; + } + // Paper end - rewrite chunk system + // Paper start - fix MC-4 + if (this instanceof ItemEntity) { + if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) { diff --git a/patches/server/MC-Utils.patch b/patches/server/MC-Utils.patch index 094893b897..7cef0ea5b6 100644 --- a/patches/server/MC-Utils.patch +++ b/patches/server/MC-Utils.patch @@ -856,6 +856,137 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return this.map.values().iterator(); + } +} +diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java +@@ -0,0 +0,0 @@ ++package com.destroystokyo.paper.util.maplist; ++ ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++ ++/** ++ * @author Spottedleaf ++ */ ++public final class ReferenceList implements Iterable { ++ ++ protected final Reference2IntOpenHashMap referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f); ++ { ++ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ protected static final Object[] EMPTY_LIST = new Object[0]; ++ ++ protected Object[] references = EMPTY_LIST; ++ protected int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public boolean contains(final E obj) { ++ return this.referenceToIndex.containsKey(obj); ++ } ++ ++ public boolean remove(final E obj) { ++ final int index = this.referenceToIndex.removeInt(obj); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the object at the end to this index ++ final int endIndex = --this.count; ++ final E end = (E)this.references[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.referenceToIndex.put(end, index); // update index ++ } ++ this.references[index] = end; ++ this.references[endIndex] = null; ++ ++ return true; ++ } ++ ++ public boolean add(final E obj) { ++ final int count = this.count; ++ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ Object[] list = this.references; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = obj; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public E getChecked(final int index) { ++ if (index < 0 || index >= this.count) { ++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); ++ } ++ return (E)this.references[index]; ++ } ++ ++ public E getUnchecked(final int index) { ++ return (E)this.references[index]; ++ } ++ ++ public Object[] getRawData() { ++ return this.references; ++ } ++ ++ public void clear() { ++ this.referenceToIndex.clear(); ++ Arrays.fill(this.references, 0, this.count, null); ++ this.count = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return new Iterator<>() { ++ private E lastRet; ++ private int current; ++ ++ @Override ++ public boolean hasNext() { ++ return this.current < ReferenceList.this.count; ++ } ++ ++ @Override ++ public E next() { ++ if (this.current >= ReferenceList.this.count) { ++ throw new NoSuchElementException(); ++ } ++ return this.lastRet = (E)ReferenceList.this.references[this.current++]; ++ } ++ ++ @Override ++ public void remove() { ++ final E lastRet = this.lastRet; ++ ++ if (lastRet == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastRet = null; ++ ++ ReferenceList.this.remove(lastRet); ++ --this.current; ++ } ++ }; ++ } ++} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -4544,6 +4675,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; @@ -4775,30 +4907,54 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + } + -+ public static void onChunkBorder(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { + chunk.playerChunk = holder; + } + -+ public static void onChunkNotBorder(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { + + } + -+ public static void onChunkTicking(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.add(chunk); + } + -+ public static void onChunkNotTicking(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.remove(chunk); + } + -+ public static void onChunkEntityTicking(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.add(chunk); + } + -+ public static void onChunkNotEntityTicking(LevelChunk chunk, ChunkHolder holder) { ++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.remove(chunk); + } + ++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { ++ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); ++ } ++ ++ public static int getSendViewDistance(final ServerPlayer player) { ++ return getLoadViewDistance(player); ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.getLevel(); ++ if (level == null) { ++ return Bukkit.getViewDistance() + 1; ++ } ++ return level.chunkSource.chunkMap.getEffectiveViewDistance() + 1; ++ } ++ ++ public static int getTickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.getLevel(); ++ if (level == null) { ++ return Bukkit.getSimulationDistance(); ++ } ++ return level.chunkSource.chunkMap.distanceManager.getSimulationDistance(); ++ } ++ + private ChunkSystem() { + throw new RuntimeException(); + } @@ -5904,6 +6060,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } protected void purgeStaleTickets() { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + this.playerTicketManager.updateViewDistance(viewDistance); + } + ++ // Paper start ++ public int getSimulationDistance() { ++ return this.simulationDistance; ++ } ++ // Paper end ++ + public void updateSimulationDistance(int simulationDistance) { + if (simulationDistance != this.simulationDistance) { + this.simulationDistance = simulationDistance; @@ -0,0 +0,0 @@ public abstract class DistanceManager { } @@ -6339,6 +6508,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent // CraftBukkit end ++ public boolean isRealPlayer; // Paper + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, @Nullable ProfilePublicKey publicKey) { @@ -6396,6 +6566,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @Override public BlockState getBlockState(BlockPos pos) { return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos); +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -0,0 +0,0 @@ public abstract class PlayerList { + } + + public void placeNewPlayer(Connection connection, ServerPlayer player) { ++ player.isRealPlayer = true; // Paper + GameProfile gameprofile = player.getGameProfile(); + GameProfileCache usercache = this.server.getProfileCache(); + Optional optional = usercache.get(gameprofile.getId()); diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java @@ -6864,7 +7046,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 // CraftBukkit end + // Paper start -+ public final com.destroystokyo.paper.util.maplist.EntityList entities = new com.destroystokyo.paper.util.maplist.EntityList(); + public @Nullable ChunkHolder playerChunk; + + static final int NEIGHBOUR_CACHE_RADIUS = 3; diff --git a/patches/server/Make-CallbackExecutor-strict-again.patch b/patches/server/Make-CallbackExecutor-strict-again.patch deleted file mode 100644 index b7c0435254..0000000000 --- a/patches/server/Make-CallbackExecutor-strict-again.patch +++ /dev/null @@ -1,48 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Fri, 24 Apr 2020 09:06:15 -0700 -Subject: [PATCH] Make CallbackExecutor strict again - -The correct fix for double scheduling is to avoid it. The reason -this class is used is because double scheduling causes issues -elsewhere, and it acts as an explicit detector of what double -schedules. Effectively, use the callback executor as a tool of -finding issues rather than hiding these issues. - -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 - public final CallbackExecutor callbackExecutor = new CallbackExecutor(); - public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { - -- private final java.util.Queue queue = new java.util.ArrayDeque<>(); -+ private Runnable queued; // Paper - revert CB changes - - @Override - public void execute(Runnable runnable) { -- this.queue.add(runnable); -+ // Paper start - revert CB changes -+ org.spigotmc.AsyncCatcher.catchOp("Callback Executor execute"); -+ if (this.queued != null) { -+ LOGGER.error("Failed to schedule runnable", new IllegalStateException("Already queued")); -+ throw new IllegalStateException("Already queued"); -+ } -+ this.queued = runnable; -+ // Paper end - revert CB changes - } - - @Override - public void run() { -- Runnable task; -- while ((task = this.queue.poll()) != null) { -+ // Paper start - revert CB changes -+ org.spigotmc.AsyncCatcher.catchOp("Callback Executor execute"); -+ Runnable task = this.queued; -+ if (task != null) { -+ this.queued = null; -+ // Paper end - revert CB changes - task.run(); - } - } diff --git a/patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch b/patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch index d8b7d91dce..acf5ee6f17 100644 --- a/patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch +++ b/patches/server/Make-sure-inlined-getChunkAt-has-inlined-logic-for-l.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Sun, 20 Sep 2020 16:10:49 -0700 Subject: [PATCH] Make sure inlined getChunkAt has inlined logic for loaded chunks diff --git a/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch b/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch deleted file mode 100644 index 0c91964dac..0000000000 --- a/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Brokkonaut -Date: Tue, 7 Feb 2017 16:55:35 -0600 -Subject: [PATCH] Make targetSize more aggressive in the chunk unload queue - - -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 - this.entityMap = new Int2ObjectOpenHashMap(); - this.chunkTypeCache = new Long2ByteOpenHashMap(); - this.chunkSaveCooldowns = new Long2LongOpenHashMap(); -- this.unloadQueue = Queues.newConcurrentLinkedQueue(); -+ this.unloadQueue = new com.destroystokyo.paper.utils.CachedSizeConcurrentLinkedQueue<>(); // Paper - need constant-time size() - this.structureTemplateManager = structureTemplateManager; - Path path = session.getDimensionPath(world.dimension()); - -@@ -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()) { - 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 - } - } - -- int k = Math.max(0, this.unloadQueue.size() - 2000); -+ int k = Math.max(100, this.unloadQueue.size() - 2000); // Paper - Unload more than just up to queue size 2000 - - Runnable runnable; - diff --git a/patches/server/Manually-inline-methods-in-BlockPosition.patch b/patches/server/Manually-inline-methods-in-BlockPosition.patch index 7688ad56d8..ebd1ff6c94 100644 --- a/patches/server/Manually-inline-methods-in-BlockPosition.patch +++ b/patches/server/Manually-inline-methods-in-BlockPosition.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 6 Jul 2020 22:48:48 -0700 Subject: [PATCH] Manually inline methods in BlockPosition diff --git a/patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch b/patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch index 25cfb3448a..436bf7f50f 100644 --- a/patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch +++ b/patches/server/Name-craft-scheduler-threads-according-to-the-plugin.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Sun, 19 Jul 2020 15:17:01 -0700 Subject: [PATCH] Name craft scheduler threads according to the plugin using them diff --git a/patches/server/Only-send-Dragon-Wither-Death-sounds-to-same-world.patch b/patches/server/Only-send-Dragon-Wither-Death-sounds-to-same-world.patch index 32e5da134a..73562a80a6 100644 --- a/patches/server/Only-send-Dragon-Wither-Death-sounds-to-same-world.patch +++ b/patches/server/Only-send-Dragon-Wither-Death-sounds-to-same-world.patch @@ -10,14 +10,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java @@ -0,0 +0,0 @@ public class EnderDragon extends Mob implements Enemy { - if (this.dragonDeathTime == 1 && !this.isSilent()) { // CraftBukkit start - Use relative location for far away sounds // this.world.b(1028, this.getChunkCoordinates(), 0); -- int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; + int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; - for (net.minecraft.server.level.ServerPlayer player : this.level.getServer().getPlayerList().players) { -+ //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API + for (net.minecraft.server.level.ServerPlayer player : (List) ((ServerLevel)level).players()) { -+ final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch double deltaX = this.getX() - player.getX(); double deltaZ = this.getZ() - player.getZ(); double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; @@ -26,14 +23,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java @@ -0,0 +0,0 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob - if (!this.isSilent()) { // CraftBukkit start - Use relative location for far away sounds // this.world.globalLevelEvent(1023, new BlockPosition(this), 0); -- int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; + int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; - for (ServerPlayer player : (List) MinecraftServer.getServer().getPlayerList().players) { -+ //int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API + for (ServerPlayer player : (List)this.level.players()) { // Paper -+ final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch double deltaX = this.getX() - player.getX(); double deltaZ = this.getZ() - player.getZ(); double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; diff --git a/patches/server/Oprimise-map-impl-for-tracked-players.patch b/patches/server/Oprimise-map-impl-for-tracked-players.patch index aeb3f65b13..808a2c2602 100644 --- a/patches/server/Oprimise-map-impl-for-tracked-players.patch +++ b/patches/server/Oprimise-map-impl-for-tracked-players.patch @@ -10,14 +10,6 @@ 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 @@ import org.slf4j.Logger; - import org.bukkit.craftbukkit.generator.CustomChunkGenerator; - import org.bukkit.entity.Player; - // CraftBukkit end -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper - - public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { - @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider final Entity entity; private final int range; diff --git a/patches/server/Optimise-ArraySetSorted-removeIf.patch b/patches/server/Optimise-ArraySetSorted-removeIf.patch deleted file mode 100644 index f54572ca32..0000000000 --- a/patches/server/Optimise-ArraySetSorted-removeIf.patch +++ /dev/null @@ -1,88 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Wed, 15 Apr 2020 18:23:28 -0700 -Subject: [PATCH] Optimise ArraySetSorted#removeIf - -Remove iterator allocation and ensure the call is always O(n) - -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 { - protected void purgeStaleTickets() { - ++this.ticketTickCounter; - ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -+ // Paper start - use optimised removeIf -+ long[] currChunk = new long[1]; -+ long ticketCounter = DistanceManager.this.ticketTickCounter; -+ java.util.function.Predicate> removeIf = (ticket) -> { -+ final boolean ret = ticket.timedOut(ticketCounter); -+ if (ret) { -+ this.tickingTicketsTracker.removeTicket(currChunk[0], ticket); -+ } -+ return ret; -+ }; -+ // Paper end - use optimised removeIf - - while (objectiterator.hasNext()) { - Entry>> entry = (Entry) objectiterator.next(); -- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); -- boolean flag = false; -+ // Paper start - use optimised removeIf -+ Iterator> iterator = null; -+ currChunk[0] = entry.getLongKey(); -+ boolean flag = entry.getValue().removeIf(removeIf); - -- while (iterator.hasNext()) { -+ while (false && iterator.hasNext()) { -+ // Paper end - use optimised removeIf - Ticket ticket = (Ticket) iterator.next(); - - if (ticket.timedOut(this.ticketTickCounter)) { -diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/util/SortedArraySet.java -+++ b/src/main/java/net/minecraft/util/SortedArraySet.java -@@ -0,0 +0,0 @@ public class SortedArraySet extends AbstractSet { - this.contents = (T[])castRawArray(new Object[initialCapacity]); - } - } -+ // Paper start - optimise removeIf -+ @Override -+ public boolean removeIf(java.util.function.Predicate filter) { -+ // prev. impl used an iterator, which could be n^2 and creates garbage -+ int i = 0, len = this.size; -+ T[] backingArray = this.contents; -+ -+ for (;;) { -+ if (i >= len) { -+ return false; -+ } -+ if (!filter.test(backingArray[i])) { -+ ++i; -+ continue; -+ } -+ break; -+ } -+ -+ // we only want to write back to backingArray if we really need to -+ -+ int lastIndex = i; // this is where new elements are shifted to -+ -+ for (; i < len; ++i) { -+ T curr = backingArray[i]; -+ if (!filter.test(curr)) { // if test throws we're screwed -+ backingArray[lastIndex++] = curr; -+ } -+ } -+ -+ // cleanup end -+ Arrays.fill(backingArray, lastIndex, len, null); -+ this.size = lastIndex; -+ return true; -+ } -+ // Paper end - optimise removeIf - - public static > SortedArraySet create() { - return create(10); diff --git a/patches/server/Optimise-IEntityAccess-getPlayerByUUID.patch b/patches/server/Optimise-IEntityAccess-getPlayerByUUID.patch index c012410f7f..652d9985d7 100644 --- a/patches/server/Optimise-IEntityAccess-getPlayerByUUID.patch +++ b/patches/server/Optimise-IEntityAccess-getPlayerByUUID.patch @@ -10,8 +10,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- 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 + } + // Paper end - rewrite chunk system + // Paper start - optimise getPlayerByUUID + @Nullable diff --git a/patches/server/Optimise-WorldServer-notify.patch b/patches/server/Optimise-WorldServer-notify.patch deleted file mode 100644 index 76714eeb4b..0000000000 --- a/patches/server/Optimise-WorldServer-notify.patch +++ /dev/null @@ -1,337 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 9 Jul 2020 13:34:59 -0700 -Subject: [PATCH] Optimise WorldServer#notify - -Iterating over all of the navigators in the world is pretty expensive. -Instead, only iterate over navigators in the current region that are -eligible for repathing. - -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 - public final io.papermc.paper.chunk.SingleThreadChunkRegionManager dataRegionManager; - - public static final class DataRegionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionData { -+ // Paper start - optimise notify() -+ private io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet navigators; -+ -+ public io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { -+ return this.navigators; -+ } -+ -+ public boolean addToNavigators(final Mob navigator) { -+ if (this.navigators == null) { -+ this.navigators = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(); -+ } -+ return this.navigators.add(navigator); -+ } -+ -+ public boolean removeFromNavigators(final Mob navigator) { -+ if (this.navigators == null) { -+ return false; -+ } -+ return this.navigators.remove(navigator); -+ } -+ // Paper end - optimise notify() - } - - public static final class DataRegionSectionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSectionData { - -+ // Paper start - optimise notify() -+ private io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet navigators; -+ -+ public io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet getNavigators() { -+ return this.navigators; -+ } -+ -+ public boolean addToNavigators(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { -+ if (this.navigators == null) { -+ this.navigators = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(); -+ } -+ final boolean ret = this.navigators.add(navigator); -+ if (ret) { -+ final DataRegionData data = (DataRegionData)section.getRegion().regionData; -+ if (!data.addToNavigators(navigator)) { -+ throw new IllegalStateException(); -+ } -+ } -+ return ret; -+ } -+ -+ public boolean removeFromNavigators(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, final Mob navigator) { -+ if (this.navigators == null) { -+ return false; -+ } -+ final boolean ret = this.navigators.remove(navigator); -+ if (ret) { -+ final DataRegionData data = (DataRegionData)section.getRegion().regionData; -+ if (!data.removeFromNavigators(navigator)) { -+ throw new IllegalStateException(); -+ } -+ } -+ return ret; -+ } -+ // Paper end - optimise notify() -+ - @Override - public void removeFromRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, - final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region from) { - final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; - final DataRegionData fromData = (DataRegionData)from.regionData; -+ // Paper start - optimise notify() -+ if (sectionData.navigators != null) { -+ for (final Iterator iterator = sectionData.navigators.unsafeIterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ if (!fromData.removeFromNavigators(iterator.next())) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ // Paper end - optimise notify() - } - - @Override -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; - final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData; - final DataRegionData newRegionData = (DataRegionData)newRegion.regionData; -+ // Paper start - optimise notify() -+ if (sectionData.navigators != null) { -+ for (final Iterator iterator = sectionData.navigators.unsafeIterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ if (!newRegionData.addToNavigators(iterator.next())) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ // Paper end - optimise notify() - } - } - -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 void tickNonPassenger(Entity entity) { - // Paper start - log detailed entity tick information - io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); -+ if (!entity.isRemoved()) this.entityManager.updateNavigatorsInRegion(entity); // Paper - optimise notify - try { - if (currentlyTickingEntity.get() == null) { - currentlyTickingEntity.lazySet(entity); -@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { - - if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { - List list = new ObjectArrayList(); -- Iterator iterator = this.navigatingMobs.iterator(); -+ // Paper start - optimise notify() -+ io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region region = this.getChunkSource().chunkMap.dataRegionManager.getRegion(pos.getX() >> 4, pos.getZ() >> 4); -+ if (region == null) { -+ return; -+ } -+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet navigatorsFromRegion = ((ChunkMap.DataRegionData)region.regionData).getNavigators(); -+ if (navigatorsFromRegion == null) { -+ return; -+ } -+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = navigatorsFromRegion.iterator(); - -- while (iterator.hasNext()) { -+ try { while (iterator.hasNext()) { // Paper end - optimise notify() - // CraftBukkit start - fix SPIGOT-6362 - Mob entityinsentient; - try { -@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { - - try { - this.isUpdatingNavigations = true; -- iterator = list.iterator(); -+ // Paper start - optimise notify() -+ Iterator navigationIterator = list.iterator(); - -- while (iterator.hasNext()) { -- PathNavigation navigationabstract1 = (PathNavigation) iterator.next(); -+ while (navigationIterator.hasNext()) { -+ PathNavigation navigationabstract1 = navigationIterator.next(); -+ // Paper end - optimise notify() - - navigationabstract1.recomputePath(); - } - } finally { - this.isUpdatingNavigations = false; - } -+ // Paper start - optimise notify() -+ } finally { -+ iterator.finishedIterating(); -+ } -+ // Paper end - optimise notify() - - } - } // Paper -@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { - - public void onTickingStart(Entity entity) { - ServerLevel.this.entityTickList.add(entity); -+ ServerLevel.this.entityManager.addNavigatorsIfPathingToRegion(entity); // Paper - optimise notify - } - - public void onTickingEnd(Entity entity) { - ServerLevel.this.entityTickList.remove(entity); -+ ServerLevel.this.entityManager.removeNavigatorsFromData(entity); // Paper - optimise notify - // Paper start - Reset pearls when they stop being ticked - if (paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { - pearl.cachedOwner = null; -diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java -+++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java -@@ -0,0 +0,0 @@ import net.minecraft.world.phys.Vec3; - - public abstract class PathNavigation { - private static final int MAX_TIME_RECOMPUTE = 20; -- protected final Mob mob; -+ protected final Mob mob; public final Mob getEntity() { return this.mob; } // Paper - public accessor - protected final Level level; - @Nullable - protected Path path; -@@ -0,0 +0,0 @@ public abstract class PathNavigation { - protected long lastTimeoutCheck; - protected double timeoutLimit; - protected float maxDistanceToWaypoint = 0.5F; -- protected boolean hasDelayedRecomputation; -+ protected boolean hasDelayedRecomputation; protected final boolean needsPathRecalculation() { return this.hasDelayedRecomputation; } // Paper - public accessor - protected long timeLastRecompute; - protected NodeEvaluator nodeEvaluator; - @Nullable -@@ -0,0 +0,0 @@ public abstract class PathNavigation { - public boolean shouldRecomputePath(BlockPos pos) { - if (this.hasDelayedRecomputation) { - return false; -- } else if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { -+ } else if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { // Paper - diff on change - needed for isViableForPathRecalculationChecking() - Node node = this.path.getEndNode(); - Vec3 vec3 = new Vec3(((double)node.x + this.mob.getX()) / 2.0D, ((double)node.y + this.mob.getY()) / 2.0D, ((double)node.z + this.mob.getZ()) / 2.0D); - return pos.closerToCenterThan(vec3, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex())); -@@ -0,0 +0,0 @@ public abstract class PathNavigation { - public boolean isStuck() { - return this.isStuck; - } -+ -+ // Paper start -+ public boolean isViableForPathRecalculationChecking() { -+ return !this.needsPathRecalculation() && -+ (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0); -+ } -+ // Paper end - } -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 - } - // CraftBukkit end - -+ // Paper start - optimise notify() -+ public final void removeNavigatorsFromData(Entity entity, final int chunkX, final int chunkZ) { -+ if (!(entity instanceof net.minecraft.world.entity.Mob)) { -+ return; -+ } -+ io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section = -+ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(chunkX, chunkZ); -+ if (section != null) { -+ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; -+ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); -+ } -+ } -+ -+ public final void removeNavigatorsFromData(Entity entity) { -+ if (!(entity instanceof net.minecraft.world.entity.Mob)) { -+ return; -+ } -+ BlockPos entityPos = entity.blockPosition(); -+ io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section = -+ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); -+ if (section != null) { -+ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; -+ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); -+ } -+ } -+ -+ public final void addNavigatorsIfPathingToRegion(Entity entity) { -+ if (!(entity instanceof net.minecraft.world.entity.Mob)) { -+ return; -+ } -+ BlockPos entityPos = entity.blockPosition(); -+ io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section = -+ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); -+ if (section != null) { -+ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; -+ if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { -+ sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); -+ } -+ } -+ } -+ -+ public final void updateNavigatorsInRegion(Entity entity) { -+ if (!(entity instanceof net.minecraft.world.entity.Mob)) { -+ return; -+ } -+ BlockPos entityPos = entity.blockPosition(); -+ io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section = -+ this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.getRegionSection(entityPos.getX() >> 4, entityPos.getZ() >> 4); -+ if (section != null) { -+ net.minecraft.server.level.ChunkMap.DataRegionSectionData sectionData = (net.minecraft.server.level.ChunkMap.DataRegionSectionData)section.sectionData; -+ if (((net.minecraft.world.entity.Mob)entity).getNavigation().isViableForPathRecalculationChecking()) { -+ sectionData.addToNavigators(section, ((net.minecraft.world.entity.Mob)entity)); -+ } else { -+ sectionData.removeFromNavigators(section, ((net.minecraft.world.entity.Mob)entity)); -+ } -+ } -+ } -+ // Paper end - optimise notify() -+ - void removeSectionIfEmpty(long sectionPos, EntitySection section) { - if (section.isEmpty()) { - this.sectionStorage.remove(sectionPos); -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - @Override - public void onMove() { - BlockPos blockposition = this.entity.blockPosition(); -- long i = SectionPos.asLong(blockposition); -+ long i = SectionPos.asLong(blockposition); final long newSectionPos = i; // Paper - diff on change, new position section - - if (i != this.currentSectionKey) { - PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Paper -- Visibility visibility = this.currentSection.getStatus(); -+ Visibility visibility = this.currentSection.getStatus(); final Visibility oldVisibility = visibility; // Paper - diff on change - this should be OLD section visibility -+ // Paper start -+ int shift = PersistentEntitySectionManager.this.entitySliceManager.world.getChunkSource().chunkMap.dataRegionManager.regionChunkShift; -+ int oldChunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(this.currentSectionKey); -+ int oldChunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(this.currentSectionKey); -+ int oldRegionX = oldChunkX >> shift; -+ int oldRegionZ = oldChunkZ >> shift; -+ -+ int newRegionX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(newSectionPos) >> shift; -+ int newRegionZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(newSectionPos) >> shift; -+ -+ if (oldRegionX != newRegionX || oldRegionZ != newRegionZ) { -+ PersistentEntitySectionManager.this.removeNavigatorsFromData((Entity)this.entity, oldChunkX, oldChunkZ); -+ } -+ // Paper end - - if (!this.currentSection.remove(this.entity)) { - PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", new Object[]{this.entity, SectionPos.of(this.currentSectionKey), i}); -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - entitysection.add(this.entity); - this.currentSection = entitysection; - this.currentSectionKey = i; -+ // Paper start -+ if ((oldRegionX != newRegionX || oldRegionZ != newRegionZ) && oldVisibility.isTicking() && entitysection.getStatus().isTicking()) { -+ PersistentEntitySectionManager.this.addNavigatorsIfPathingToRegion((Entity)this.entity); -+ } -+ // Paper end - this.updateStatus(visibility, entitysection.getStatus()); - } - diff --git a/patches/server/Optimise-chunk-tick-iteration.patch b/patches/server/Optimise-chunk-tick-iteration.patch index dc71d08e9a..61aa1b9543 100644 --- a/patches/server/Optimise-chunk-tick-iteration.patch +++ b/patches/server/Optimise-chunk-tick-iteration.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Thu, 7 May 2020 05:48:54 -0700 Subject: [PATCH] Optimise chunk tick iteration @@ -75,14 +75,23 @@ 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 @@ import org.bukkit.craftbukkit.generator.CustomChunkGenerator; + import org.bukkit.entity.Player; + // CraftBukkit end + ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper ++ + public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider { + + private static final byte CHUNK_TYPE_REPLACEABLE = -1; @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private final Queue unloadQueue; int viewDistance; public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper + public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); - // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() - public final CallbackExecutor callbackExecutor = new CallbackExecutor(); + // Paper - rewrite chunk system + 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 @@ -147,14 +156,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 ChunkPos chunkcoordintpair = chunk1.getPos(); - if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkproviderserver_a.holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning -+ if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning ++ if ((true || this.level.isNaturalSpawningAllowed(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning // Paper - the chunk is known ticking chunk1.incrementInhabitedTime(j); - if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkproviderserver_a.holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning + if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); } -@@ -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); } } diff --git a/patches/server/Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/Optimise-collision-checking-in-player-move-packet-ha.patch index ea622f3c52..e01cc9fe25 100644 --- a/patches/server/Optimise-collision-checking-in-player-move-packet-ha.patch +++ b/patches/server/Optimise-collision-checking-in-player-move-packet-ha.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Thu, 2 Jul 2020 12:02:43 -0700 Subject: [PATCH] Optimise collision checking in player move packet handling diff --git a/patches/server/Optimise-general-POI-access.patch b/patches/server/Optimise-general-POI-access.patch index 655c742798..aecef814bc 100644 --- a/patches/server/Optimise-general-POI-access.patch +++ b/patches/server/Optimise-general-POI-access.patch @@ -879,15 +879,6 @@ diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager. 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 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); @@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage { } @@ -982,7 +973,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor private final Runnable setDirty; private boolean isValid; - + public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system 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 diff --git a/patches/server/Optimise-getChunkAt-calls-for-loaded-chunks.patch b/patches/server/Optimise-getChunkAt-calls-for-loaded-chunks.patch index 28344e744c..3bd61b6429 100644 --- a/patches/server/Optimise-getChunkAt-calls-for-loaded-chunks.patch +++ b/patches/server/Optimise-getChunkAt-calls-for-loaded-chunks.patch @@ -24,7 +24,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 gameprofilerfiller.incrementCounter("getChunk"); @@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - if (Thread.currentThread() != this.mainThread) { + if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system return null; } else { - this.level.getProfiler().incrementCounter("getChunkNow"); diff --git a/patches/server/Optimise-getType-calls.patch b/patches/server/Optimise-getType-calls.patch index ce479f6785..bd9a466767 100644 --- a/patches/server/Optimise-getType-calls.patch +++ b/patches/server/Optimise-getType-calls.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Wed, 3 Jun 2020 11:37:13 -0700 Subject: [PATCH] Optimise getType calls diff --git a/patches/server/Optimise-nearby-player-lookups.patch b/patches/server/Optimise-nearby-player-lookups.patch index a947575ea8..44415c53e8 100644 --- a/patches/server/Optimise-nearby-player-lookups.patch +++ b/patches/server/Optimise-nearby-player-lookups.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Thu, 27 Aug 2020 16:22:52 -0700 Subject: [PATCH] Optimise nearby player lookups @@ -43,27 +43,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- 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 - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); + // Paper - rewrite chunk system + // Paper start - optimise checkDespawn + public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; + // Paper end - optimise checkDespawn -+ - // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() - public final CallbackExecutor callbackExecutor = new CallbackExecutor(); - public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { + + // 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 + trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, net.minecraft.server.ChunkSystem.getSendViewDistance(player))); + } // Paper end - use distance map to optimise entity tracker - // Note: players need to be explicitly added to distance maps before they can be updated - this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn - // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.add(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerMobSpawnMap.remove(player); this.playerChunkTickRangeMap.remove(player); @@ -73,13 +72,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 if (this.playerMobDistanceMap != null) { this.playerMobDistanceMap.remove(player); @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, net.minecraft.server.ChunkSystem.getSendViewDistance(player))); } // Paper end - use distance map to optimise entity tracker - this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn - // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.update(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); + } + // Paper end + // Paper start @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } }); diff --git a/patches/server/Optimise-non-flush-packet-sending.patch b/patches/server/Optimise-non-flush-packet-sending.patch index c3dd3c4721..a36c0130bb 100644 --- a/patches/server/Optimise-non-flush-packet-sending.patch +++ b/patches/server/Optimise-non-flush-packet-sending.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Tue, 22 Sep 2020 01:49:19 -0700 Subject: [PATCH] Optimise non-flush packet sending diff --git a/patches/server/Optimize-Captured-TileEntity-Lookup.patch b/patches/server/Optimize-Captured-TileEntity-Lookup.patch index 1925711d6f..d169847e04 100644 --- a/patches/server/Optimize-Captured-TileEntity-Lookup.patch +++ b/patches/server/Optimize-Captured-TileEntity-Lookup.patch @@ -26,5 +26,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } + // Paper end // CraftBukkit end - return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); + return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system } diff --git a/patches/server/Optimize-CraftBlockData-Creation.patch b/patches/server/Optimize-CraftBlockData-Creation.patch index 3cbb850fd6..5345178ad4 100644 --- a/patches/server/Optimize-CraftBlockData-Creation.patch +++ b/patches/server/Optimize-CraftBlockData-Creation.patch @@ -11,8 +11,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - this.emissiveRendering = blockbase_info.emissiveRendering; this.offsetType = (BlockBehaviour.OffsetType) blockbase_info.offsetType.apply(this.asState()); + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Paper } + // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time + private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; diff --git a/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch index 40379f97f4..1a77e25386 100644 --- a/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch +++ b/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 6 Apr 2020 17:53:29 -0700 Subject: [PATCH] Optimize GoalSelector Goal.Flag Set operations diff --git a/patches/server/Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/patches/server/Optimize-Network-Manager-and-add-advanced-packet-sup.patch index 1e39689980..f6f8858407 100644 --- a/patches/server/Optimize-Network-Manager-and-add-advanced-packet-sup.patch +++ b/patches/server/Optimize-Network-Manager-and-add-advanced-packet-sup.patch @@ -185,6 +185,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } - private void flushQueue() { +- try { // Paper - add pending task queue - if (this.channel != null && this.channel.isOpen()) { - Queue queue = this.queue; - @@ -209,6 +210,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return false; + } + private boolean processQueue() { ++ try { // Paper - add pending task queue + if (this.queue.isEmpty()) return true; + // If we are on main, we are safe here in that nothing else should be processing queue off main anymore + // But if we are not on main due to login/status, the parent is synchronized on packetQueue @@ -230,6 +232,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } } + return true; + } finally { // Paper start - add pending task queue + Runnable r; + while ((r = this.pendingTasks.poll()) != null) { +@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { + } + } // Paper end - add pending task queue } + // Paper end diff --git a/patches/server/Optimize-ServerLevels-chunk-level-checking-methods.patch b/patches/server/Optimize-ServerLevels-chunk-level-checking-methods.patch deleted file mode 100644 index c00e5642b2..0000000000 --- a/patches/server/Optimize-ServerLevels-chunk-level-checking-methods.patch +++ /dev/null @@ -1,69 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 16 Apr 2020 16:13:59 -0700 -Subject: [PATCH] Optimize ServerLevels chunk level checking methods - -These can be hot functions (i.e entity ticking and block ticking), -so inline where possible, and avoid the abstraction of the -Either class. - -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 { - } - - private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { -- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos); -+ // Paper start - optimize is ticking ready type functions -+ ChunkHolder chunkHolder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return chunkHolder != null && this.chunkSource.isPositionTicking(chunkPos) && chunkHolder.isTickingReady() && this.areEntitiesLoaded(chunkPos); -+ // Paper end - } - - public boolean isPositionEntityTicking(BlockPos pos) { -- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos)); -+ return this.entityManager.canPositionTick(ChunkPos.asLong(pos)) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos)); // Paper - } - - public boolean isNaturalSpawningAllowed(BlockPos pos) { -- return this.entityManager.canPositionTick(pos); -+ return this.entityManager.canPositionTick(ChunkPos.asLong(pos)); // Paper - } - - public boolean isNaturalSpawningAllowed(ChunkPos pos) { -- return this.entityManager.canPositionTick(pos); -+ return this.entityManager.canPositionTick(pos.toLong()); // Paper - } - - private final class EntityCallbacks implements LevelCallback { -diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/ChunkPos.java -+++ b/src/main/java/net/minecraft/world/level/ChunkPos.java -@@ -0,0 +0,0 @@ public class ChunkPos { - } - - public static long asLong(BlockPos pos) { -- return asLong(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); -+ return (((long)pos.getX() >> 4) & 4294967295L) | ((((long)pos.getZ() >> 4) & 4294967295L) << 32); // Paper - inline - } - - public static int getX(long pos) { -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 LevelEntityGetter getEntityGetter() { - return this.entityGetter; - } -+ // Paper start -+ public final boolean canPositionTick(long position) { -+ return this.chunkVisibility.get(position).isTicking(); -+ } -+ // Paper end - - public boolean canPositionTick(BlockPos pos) { - return ((Visibility) this.chunkVisibility.get(ChunkPos.asLong(pos))).isTicking(); diff --git a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch index b75cccf5b4..baeefbd477 100644 --- a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch +++ b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch @@ -30,23 +30,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } // Paper end + public final io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder; // Paper - rewrite chunk system + + // Paper start - optimise anyPlayerCloseEnoughForSpawning + // cached here to avoid a map lookup + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInMobSpawnRange; + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInChunkTickRange; + // Paper end - optimise anyPlayerCloseEnoughForSpawning + - public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { - this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); - this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system + this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system + this.chunkToSaveHistory = null; 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 - 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<>(); + public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader + // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + // A note about the naming used here: + // Previously, mojang used a "spawn range" of 8 for controlling both ticking and @@ -60,16 +62,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning void addPlayerToDistanceMaps(ServerPlayer player) { + this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader int chunkX = MCUtil.getChunkCoordinate(player.getX()); int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); // Note: players need to be explicitly added to distance maps before they can be updated + this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper start - per player mob spawning if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.add(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); + this.playerMobDistanceMap.add(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - void removePlayerFromDistanceMaps(ServerPlayer player) { + this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader + // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + this.playerMobSpawnMap.remove(player); @@ -79,13 +82,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 if (this.playerMobDistanceMap != null) { this.playerMobDistanceMap.remove(player); @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int chunkX = MCUtil.getChunkCoordinate(player.getX()); int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); // Note: players need to be explicitly added to distance maps before they can be updated + this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader + this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper start - per player mob spawning if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.update(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); + this.playerMobDistanceMap.update(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.regionManagers.add(this.dataRegionManager); // Paper end @@ -210,43 +213,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- 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 static final int BLOCK_TICKING_LEVEL_THRESHOLD = 33; final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); - public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); - private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); + // Paper - rewrite chunk system - private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); -+ public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used - private final TickingTracker tickingTicketsTracker = new TickingTracker(); - private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); - // Paper start use a queue, but still keep unique requirement ++ public static final int MOB_SPAWN_RANGE = 8; // private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); // Paper - no longer used + //private final TickingTracker tickingTicketsTracker = new TickingTracker(); // Paper - no longer used + //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used + // Paper - rewrite chunk system @@ -0,0 +0,0 @@ public abstract class DistanceManager { - protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + long i = chunkcoordintpair.toLong(); - public boolean runAllUpdates(ChunkMap chunkStorage) { -- this.naturalSpawnChunkCounter.runAllUpdates(); -+ //this.f.a(); // Paper - no longer used - this.tickingTicketsTracker.runAllUpdates(); - org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper - this.playerTicketManager.runAllUpdates(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> { - return new ObjectOpenHashSet(); - })).add(player); + // Paper - no longer used - this.naturalSpawnChunkCounter.update(i, 0, true); -+ //this.f.update(i, 0, true); // Paper - no longer used - this.playerTicketManager.update(i, 0, true); - this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ //this.naturalSpawnChunkCounter.update(i, 0, true); // Paper - no longer used + //this.playerTicketManager.update(i, 0, true); // Paper - no longer used + //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used } @@ -0,0 +0,0 @@ public abstract class DistanceManager { if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully. if (objectset == null || objectset.isEmpty()) { // Paper this.playersPerChunk.remove(i); - this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); -+ //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used - this.playerTicketManager.update(i, Integer.MAX_VALUE, false); - this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ // this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); // Paper - no longer used + //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used + //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used } @@ -0,0 +0,0 @@ public abstract class DistanceManager { - // Paper end + } public int getNaturalSpawnChunkCount() { - this.naturalSpawnChunkCounter.runAllUpdates(); @@ -343,9 +337,9 @@ 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 { - // CraftBukkit end public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper + 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 diff --git a/patches/server/Optimize-indirect-passenger-iteration.patch b/patches/server/Optimize-indirect-passenger-iteration.patch index c6ac7d02d0..280f61edfd 100644 --- a/patches/server/Optimize-indirect-passenger-iteration.patch +++ b/patches/server/Optimize-indirect-passenger-iteration.patch @@ -43,7 +43,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 return () -> { return this.getIndirectPassengersStream().iterator(); }; - } +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // Paper end - rewrite chunk system public boolean hasExactlyOnePlayerPassenger() { + if (this.passengers.isEmpty()) { return false; } // Paper diff --git a/patches/server/Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/patches/server/Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch index 60b695f7a7..632db232c7 100644 --- a/patches/server/Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch +++ b/patches/server/Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch @@ -58,8 +58,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java @@ -0,0 +0,0 @@ public class EmptyLevelChunk extends LevelChunk { - this.biome = holder; - } + public void setBlockEmptinessMap(final boolean[] emptinessMap) {} + // Paper end - starlight + // Paper start + @Override diff --git a/patches/server/Paper-config-files.patch b/patches/server/Paper-config-files.patch index 1a1d303bd5..642e7fb05a 100644 --- a/patches/server/Paper-config-files.patch +++ b/patches/server/Paper-config-files.patch @@ -438,7 +438,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +package io.papermc.paper.configuration; + +import co.aikar.timings.MinecraftTimings; -+import com.destroystokyo.paper.io.chunk.ChunkTaskManager; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.constraint.Constraints; +import net.kyori.adventure.text.Component; @@ -604,15 +603,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public boolean saveEmptyScoreboardTeams = false; + } + -+ public AsyncChunks asyncChunks; ++ public ChunkSystem chunkSystem; + -+ public class AsyncChunks extends ConfigurationPart.Post { -+ public int threads = -1; -+ public transient boolean asyncChunks = false; ++ public class ChunkSystem extends ConfigurationPart.Post { ++ ++ public int ioThreads = -1; ++ public int workerThreads = -1; ++ public String genParallelism = "default"; + + @Override + public void postProcess() { -+ ChunkTaskManager.processConfiguration(this); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); + } + } + diff --git a/patches/server/Paper-dumpitem-command.patch b/patches/server/Paper-dumpitem-command.patch index 1fcac6a93b..ab224aaf7d 100644 --- a/patches/server/Paper-dumpitem-command.patch +++ b/patches/server/Paper-dumpitem-command.patch @@ -18,8 +18,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 import io.papermc.paper.command.subcommands.FixLightCommand; import io.papermc.paper.command.subcommands.HeapDumpCommand; @@ -0,0 +0,0 @@ public final class PaperCommand extends Command { - 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()); + commands.put(Set.of("dumpitem"), new DumpItemCommand()); diff --git a/patches/server/Per-Player-View-Distance-API-placeholders.patch b/patches/server/Per-Player-View-Distance-API-placeholders.patch deleted file mode 100644 index 0571114375..0000000000 --- a/patches/server/Per-Player-View-Distance-API-placeholders.patch +++ /dev/null @@ -1,112 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Zach Brown <1254957+zachbr@users.noreply.github.com> -Date: Mon, 6 May 2019 01:29:25 -0400 -Subject: [PATCH] Per-Player View Distance API placeholders - -I hope to look at this more in-depth soon. It appears doable. -However this should not block the update. - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- 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 { - return (CraftPlayer) super.getBukkitEntity(); - } - // CraftBukkit end -+ -+ public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - return world.spigotConfig.simulationDistance; - } - // Spigot end -+ // Paper start - view distance api -+ @Override -+ public void setViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ -+ @Override -+ public void setSimulationDistance(int simulationDistance) { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ -+ @Override -+ public int getNoTickViewDistance() { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ -+ @Override -+ public void setNoTickViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ -+ @Override -+ public int getSendViewDistance() { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ -+ @Override -+ public void setSendViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException(); //TODO -+ } -+ // Paper end - view distance api - - // Spigot start - private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot() -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message); - } - } -+ -+ @Override -+ public int getViewDistance() { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public void setViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public int getSimulationDistance() { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public void setSimulationDistance(int simulationDistance) { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public int getNoTickViewDistance() { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public void setNoTickViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public int getSendViewDistance() { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } -+ -+ @Override -+ public void setSendViewDistance(int viewDistance) { -+ throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ } - // Paper end - - @Override diff --git a/patches/server/PlayerNaturallySpawnCreaturesEvent.patch b/patches/server/PlayerNaturallySpawnCreaturesEvent.patch index d87fb99002..37c8623bd2 100644 --- a/patches/server/PlayerNaturallySpawnCreaturesEvent.patch +++ b/patches/server/PlayerNaturallySpawnCreaturesEvent.patch @@ -76,5 +76,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 // CraftBukkit end + public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper + public boolean isRealPlayer; // Paper public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper - diff --git a/patches/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch b/patches/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch deleted file mode 100644 index f75b29accd..0000000000 --- a/patches/server/Prevent-unload-calls-removing-tickets-for-sync-loads.patch +++ /dev/null @@ -1,80 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 18 Jun 2020 18:23:20 -0700 -Subject: [PATCH] Prevent unload() calls removing tickets for sync loads - - -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, TicketType.ASYNC_LOAD); // Paper - add additional tickets to preserve -+ ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.ASYNC_LOAD, TicketType.REQUIRED_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 completablefuture; - } - -+ private long syncLoadCounter; // Paper - prevent plugin unloads from removing our ticket -+ - 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); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel()); - currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); - } -+ final Long identifier; // Paper - prevent plugin unloads from removing our ticket - if (create && !currentlyUnloading) { - // CraftBukkit end - this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); -+ identifier = Long.valueOf(this.syncLoadCounter++); // Paper - prevent plugin unloads from removing our ticket -+ this.distanceManager.addTicket(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Paper - prevent plugin unloads from removing our ticket - if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority - if (this.chunkAbsent(playerchunk, l)) { - ProfilerFiller gameprofilerfiller = this.level.getProfiler(); -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - playerchunk = this.getVisibleChunkIfPresent(k); - gameprofilerfiller.pop(); - if (this.chunkAbsent(playerchunk, l)) { -+ this.distanceManager.removeTicket(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Paper - throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); - } - } -- } - -+ } else { identifier = null; } // Paper - prevent plugin unloads from removing our ticket - // Paper start - Chunk priority - CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); -+ // Paper start - prevent plugin unloads from removing our ticket -+ if (create && !currentlyUnloading) { -+ future.thenAcceptAsync((either) -> { -+ ServerChunkCache.this.distanceManager.removeTicket(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); -+ }, ServerChunkCache.this.mainThreadProcessor); -+ } -+ // Paper end - prevent plugin unloads from removing our ticket - if (isUrgent) { - future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); - } -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 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 final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail - - public static TicketType create(String name, Comparator argumentComparator) { - return new TicketType<>(name, argumentComparator, 0L); diff --git a/patches/server/Put-world-into-worldlist-before-initing-the-world.patch b/patches/server/Put-world-into-worldlist-before-initing-the-world.patch index 202c2210b4..b8c1cb52b8 100644 --- a/patches/server/Put-world-into-worldlist-before-initing-the-world.patch +++ b/patches/server/Put-world-into-worldlist-before-initing-the-world.patch @@ -38,4 +38,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper - move up this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); - internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API + //internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system diff --git a/patches/server/Reduce-Either-Optional-allocation.patch b/patches/server/Reduce-Either-Optional-allocation.patch index 738c666afd..ce6fe7068b 100644 --- a/patches/server/Reduce-Either-Optional-allocation.patch +++ b/patches/server/Reduce-Either-Optional-allocation.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 6 Apr 2020 18:35:09 -0700 Subject: [PATCH] Reduce Either Optional allocation diff --git a/patches/server/Reduce-MutableInt-allocations-from-light-engine.patch b/patches/server/Reduce-MutableInt-allocations-from-light-engine.patch index c0103698e2..786cba55fa 100644 --- a/patches/server/Reduce-MutableInt-allocations-from-light-engine.patch +++ b/patches/server/Reduce-MutableInt-allocations-from-light-engine.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 27 Apr 2020 02:48:06 -0700 Subject: [PATCH] Reduce MutableInt allocations from light engine diff --git a/patches/server/Reduce-allocation-of-Vec3D-by-entity-tracker.patch b/patches/server/Reduce-allocation-of-Vec3D-by-entity-tracker.patch index 872e30ee8f..1d5a98626f 100644 --- a/patches/server/Reduce-allocation-of-Vec3D-by-entity-tracker.patch +++ b/patches/server/Reduce-allocation-of-Vec3D-by-entity-tracker.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 27 Apr 2020 00:04:16 -0700 Subject: [PATCH] Reduce allocation of Vec3D by entity tracker @@ -31,7 +31,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + double vec3d_dx = player.getX() - this.entity.getX(); + double vec3d_dz = player.getZ() - this.entity.getZ(); + // Paper end - remove allocation of Vec3D here - double d0 = (double) Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); + double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance - double d1 = vec3d.x * vec3d.x + vec3d.z * vec3d.z; + double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper double d2 = d0 * d0; diff --git a/patches/server/Reduce-blockpos-allocation-from-pathfinding.patch b/patches/server/Reduce-blockpos-allocation-from-pathfinding.patch index 19d4b878bd..b18ceb449d 100644 --- a/patches/server/Reduce-blockpos-allocation-from-pathfinding.patch +++ b/patches/server/Reduce-blockpos-allocation-from-pathfinding.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Sat, 25 Apr 2020 17:10:55 -0700 Subject: [PATCH] Reduce blockpos allocation from pathfinding diff --git a/patches/server/Reduce-memory-footprint-of-NBTTagCompound.patch b/patches/server/Reduce-memory-footprint-of-NBTTagCompound.patch index b96a3a5c84..0791f0b674 100644 --- a/patches/server/Reduce-memory-footprint-of-NBTTagCompound.patch +++ b/patches/server/Reduce-memory-footprint-of-NBTTagCompound.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 6 Apr 2020 17:39:25 -0700 Subject: [PATCH] Reduce memory footprint of NBTTagCompound diff --git a/patches/server/Remove-streams-for-villager-AI.patch b/patches/server/Remove-streams-for-villager-AI.patch index 5ea6d6fd78..524f34f2a1 100644 --- a/patches/server/Remove-streams-for-villager-AI.patch +++ b/patches/server/Remove-streams-for-villager-AI.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Thu, 27 Aug 2020 20:51:40 -0700 Subject: [PATCH] Remove streams for villager AI diff --git a/patches/server/Replace-player-chunk-loader-system.patch b/patches/server/Replace-player-chunk-loader-system.patch deleted file mode 100644 index 903a7123ee..0000000000 --- a/patches/server/Replace-player-chunk-loader-system.patch +++ /dev/null @@ -1,2270 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 24 Jan 2021 20:27:32 -0800 -Subject: [PATCH] Replace player chunk loader system - -The old one has undebuggable problems. Rewriting seems -the most sensible option. - -This new player chunk manager will also strictly rate limit -chunk sends so that netty threads do not get overloaded, whether -it be from the anti-xray logic or the compression itself. - -Chunk loading is also rate limited in the same manner, so this -will result in a maximum responsiveness for change. - -Config: -``` -chunk-loading: - min-load-radius: 2 - max-concurrent-sends: 2 - autoconfig-send-distance: true - target-player-chunk-send-rate: 100.0 - global-max-chunk-send-rate: -1 - enable-frustum-priority: false - global-max-chunk-load-rate: -1.0 - player-max-concurrent-loads: 25.0 - global-max-concurrent-loads: 500.0 -``` - -min-load-radius - The radius of chunks around a player that -are not throttled for loading. The number of chunks -affected is actually the configured value plus one as this -config controls the chunks the client will be able to render. - -max-concurrent-sends - The maximum number of chunks that -can be queued to send at any given time. Low values -are generally going to solve server-sided networking -bottlenecks like anti-xray and chunk compression. Client -side networking is unlikely to be helped (i.e this wont help -people running off McDonald's wifi). - -autoconfig-send-distance - Whether to try to use the client's -view distance for the send view distance in the server. In the -case that no plugin has explicitly set the send distance and -the client view distance is less-than the server's send distance, -the client's view distance will be used. This will not affect -tick view distance or no-tick view distance. - -target-player-chunk-send-rate - The maximum chunk send rate -an individual player will have. -1 means no limit - -global-max-chunk-send-rate - The maximum chunk send rate for -the whole server. -1 means no limit - -enable-frustum-priority - Whether chunks in front of a player -are prioritised to load/send first. Disabled by default -because the client can bug out due to the out of order -chunk sending. - -global-max-chunk-load-rate - The maximum chunk load rate -for the whole server. -1 means no limit - -player-max-concurrent-loads and global-max-concurrent-loads -The maximum number of concurrent loads for the server is -determined by the number of players on the server multiplied by the -`player-max-concurrent-loads`. It is then limited to -whatever `global-max-concurrent-loads` is configured to. - -diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/co/aikar/timings/TimingsExport.java -+++ b/src/main/java/co/aikar/timings/TimingsExport.java -@@ -0,0 +0,0 @@ public class TimingsExport extends Thread { - pair("gamerules", toObjectMapper(world.getWorld().getGameRules(), rule -> { - return pair(rule, world.getWorld().getGameRuleValue(rule)); - })), -- pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()) -+ // Paper start - replace chunk loader system -+ pair("ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()), -+ pair("no-ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()), -+ pair("sending-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance()) -+ // Paper end - replace chunk loader system - )); - })); - -diff --git a/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.chunk; -+ -+import com.destroystokyo.paper.util.misc.PlayerAreaMap; -+import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.IntervalledCounter; -+import io.papermc.paper.util.TickThread; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; -+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; -+import net.minecraft.server.MCUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.*; -+import net.minecraft.util.Mth; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.LevelChunk; -+import org.apache.commons.lang3.mutable.MutableObject; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.TreeSet; -+import java.util.concurrent.atomic.AtomicInteger; -+ -+public final class PlayerChunkLoader { -+ -+ public static final int MIN_VIEW_DISTANCE = 2; -+ public static final int MAX_VIEW_DISTANCE = 32; -+ -+ public static final int TICK_TICKET_LEVEL = 31; -+ public static final int LOADED_TICKET_LEVEL = 33; -+ -+ public static int getTickViewDistance(final Player player) { -+ return getTickViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getTickViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level; -+ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); -+ if (data == null) { -+ return level.chunkSource.chunkMap.playerChunkManager.getTargetTickViewDistance(); -+ } -+ return data.getTargetTickViewDistance(); -+ } -+ -+ public static int getLoadViewDistance(final Player player) { -+ return getLoadViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getLoadViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level; -+ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); -+ if (data == null) { -+ return level.chunkSource.chunkMap.playerChunkManager.getLoadDistance(); -+ } -+ return data.getLoadDistance(); -+ } -+ -+ public static int getSendViewDistance(final Player player) { -+ return getSendViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getSendViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level; -+ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); -+ if (data == null) { -+ return level.chunkSource.chunkMap.playerChunkManager.getTargetSendDistance(); -+ } -+ return data.getTargetSendViewDistance(); -+ } -+ -+ protected final ChunkMap chunkMap; -+ protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f); -+ protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f); -+ -+ protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { -+ if (p1 == p2) { -+ return 0; -+ } -+ -+ final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst(); -+ final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst(); -+ -+ final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority); -+ -+ final int lastLoadTimeCompare = Long.compare(p1.lastChunkLoad, p2.lastChunkLoad); -+ -+ if ((holder1 == null || holder2 == null || lastLoadTimeCompare == 0 || holder1.priority < 0.0 || holder2.priority < 0.0) && priorityCompare != 0) { -+ return priorityCompare; -+ } -+ -+ if (lastLoadTimeCompare != 0) { -+ return lastLoadTimeCompare; -+ } -+ -+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); -+ -+ if (idCompare != 0) { -+ return idCompare; -+ } -+ -+ // last resort -+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); -+ }); -+ -+ protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { -+ if (p1 == p2) { -+ return 0; -+ } -+ -+ final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget); -+ if (timeCompare != 0) { -+ return timeCompare; -+ } -+ -+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); -+ -+ if (idCompare != 0) { -+ return idCompare; -+ } -+ -+ // last resort -+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); -+ }); -+ -+ -+ // no throttling is applied below this VD for loading -+ -+ /** -+ * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded. -+ */ -+ public final PlayerAreaMap broadcastMap; -+ -+ /** -+ * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded. -+ */ -+ public final PlayerAreaMap loadMap; -+ -+ /** -+ * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus, -+ * this map is always representing the chunks we are actually going to load. -+ */ -+ public final PlayerAreaMap loadTicketCleanup; -+ -+ /** -+ * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen. -+ */ -+ public final PlayerAreaMap tickMap; -+ -+ /** -+ * -1 if defaulting to [load distance], else always in [2, load distance] -+ */ -+ protected int rawSendDistance = -1; -+ -+ /** -+ * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1] -+ */ -+ protected int rawLoadDistance = -1; -+ -+ /** -+ * Never -1, always in [2, 32] -+ */ -+ protected int rawTickDistance = -1; -+ -+ // methods to bridge for API -+ -+ public int getTargetTickViewDistance() { -+ return this.getTickDistance(); -+ } -+ -+ public void setTargetTickViewDistance(final int distance) { -+ this.setTickDistance(distance); -+ } -+ -+ public int getTargetNoTickViewDistance() { -+ return this.getLoadDistance() - 1; -+ } -+ -+ public void setTargetNoTickViewDistance(final int distance) { -+ this.setLoadDistance(distance == -1 ? -1 : distance + 1); -+ } -+ -+ public int getTargetSendDistance() { -+ return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance; -+ } -+ -+ public void setTargetSendDistance(final int distance) { -+ this.setSendDistance(distance); -+ } -+ -+ // internal methods -+ -+ public int getSendDistance() { -+ final int loadDistance = this.getLoadDistance(); -+ return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance); -+ } -+ -+ public void setSendDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Send distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + ", or -1, got: " + distance); -+ } -+ this.rawSendDistance = distance; -+ } -+ -+ public int getLoadDistance() { -+ final int tickDistance = this.getTickDistance(); -+ return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance); -+ } -+ -+ public void setLoadDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Load distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + ", or -1, got: " + distance); -+ } -+ this.rawLoadDistance = distance; -+ } -+ -+ public int getTickDistance() { -+ return this.rawTickDistance; -+ } -+ -+ public void setTickDistance(final int distance) { -+ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { -+ throw new IllegalArgumentException("View distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + ", got: " + distance); -+ } -+ this.rawTickDistance = distance; -+ } -+ -+ /* -+ Players have 3 different types of view distance: -+ 1. Sending view distance -+ 2. Loading view distance -+ 3. Ticking view distance -+ -+ But for configuration purposes (and API) there are: -+ 1. No-tick view distance -+ 2. Tick view distance -+ 3. Broadcast view distance -+ -+ These aren't always the same as the types we represent internally. -+ -+ Loading view distance is always max(no-tick + 1, tick + 1) -+ - no-tick has 1 added because clients need an extra radius to render chunks -+ - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking -+ -+ Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means -+ it loads chunks in radius load-view-distance + 1. -+ -+ The maximum value for send view distance is the load view distance. API can set it lower. -+ */ -+ -+ public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) { -+ this.chunkMap = chunkMap; -+ this.broadcastMap = new PlayerAreaMap(pooledHashSets, -+ null, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ); -+ }); -+ this.loadMap = new PlayerAreaMap(pooledHashSets, -+ null, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ)); -+ }); -+ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets, -+ null, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); -+ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) { -+ --PlayerChunkLoader.this.concurrentChunkLoads; -+ } -+ }); -+ this.tickMap = new PlayerAreaMap(pooledHashSets, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState.size() != 1) { -+ return; -+ } -+ LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); -+ if (chunk == null || !chunk.areNeighboursLoaded(2)) { -+ return; -+ } -+ -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ }, -+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { -+ if (newState != null) { -+ return; -+ } -+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); -+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ }); -+ } -+ -+ protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet(); -+ protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet(); -+ -+ public boolean isChunkNearPlayers(final int chunkX, final int chunkZ) { -+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); -+ -+ return playersInSendRange != null; -+ } -+ -+ public void onChunkPostProcessing(final int chunkX, final int chunkZ) { -+ this.onChunkSendReady(chunkX, chunkZ); -+ } -+ -+ private boolean chunkNeedsPostProcessing(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); -+ -+ if (chunk == null) { -+ return false; -+ } -+ -+ final LevelChunk levelChunk = chunk.getSendingChunk(); -+ -+ return levelChunk != null && !levelChunk.isPostProcessingDone; -+ } -+ -+ // rets whether the chunk is at a loaded stage that is ready to be sent to players -+ public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); -+ -+ if (chunk == null) { -+ return false; -+ } -+ -+ final LevelChunk levelChunk = chunk.getSendingChunk(); -+ -+ return levelChunk != null && levelChunk.isPostProcessingDone && this.isTargetedForPlayerLoad.contains(key); -+ } -+ -+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { -+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); -+ } -+ -+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ if (data == null) { -+ return false; -+ } -+ -+ return data.hasSentChunk(chunkX, chunkZ); -+ } -+ -+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ if (data == null) { -+ return false; -+ } -+ -+ final boolean center = data.hasSentChunk(chunkX, chunkZ); -+ if (!center) { -+ return false; -+ } -+ -+ return !(data.hasSentChunk(chunkX - 1, chunkZ) && data.hasSentChunk(chunkX + 1, chunkZ) && -+ data.hasSentChunk(chunkX, chunkZ - 1) && data.hasSentChunk(chunkX, chunkZ + 1)); -+ } -+ -+ protected int getMaxConcurrentChunkSends() { -+ return GlobalConfiguration.get().chunkLoading.maxConcurrentSends; -+ } -+ -+ protected int getMaxChunkLoads() { -+ double config = GlobalConfiguration.get().chunkLoading.playerMaxConcurrentLoads; -+ double max = GlobalConfiguration.get().chunkLoading.globalMaxConcurrentLoads; -+ return (int)Math.ceil(Math.min(config * MinecraftServer.getServer().getPlayerCount(), max <= 1.0 ? Double.MAX_VALUE : max)); -+ } -+ -+ protected long getTargetSendPerPlayerAddend() { -+ return GlobalConfiguration.get().chunkLoading.targetPlayerChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / GlobalConfiguration.get().chunkLoading.targetPlayerChunkSendRate); -+ } -+ -+ protected long getMaxSendAddend() { -+ return GlobalConfiguration.get().chunkLoading.globalMaxChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / GlobalConfiguration.get().chunkLoading.globalMaxChunkSendRate); -+ } -+ -+ public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) { -+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); -+ } -+ -+ public void onChunkSendReady(final int chunkX, final int chunkZ) { -+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); -+ -+ if (playersInSendRange == null) { -+ return; -+ } -+ -+ final Object[] rawData = playersInSendRange.getBackingSet(); -+ for (int i = 0, len = rawData.length; i < len; ++i) { -+ final Object raw = rawData[i]; -+ -+ if (!(raw instanceof ServerPlayer)) { -+ continue; -+ } -+ this.onChunkSendReady((ServerPlayer)raw, chunkX, chunkZ); -+ } -+ } -+ -+ public void onChunkSendReady(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ -+ if (data == null) { -+ return; -+ } -+ -+ if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) { -+ // if we don't have player tickets, then the load logic will pick this up and queue to send -+ return; -+ } -+ -+ if (!data.chunksToBeSent.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ // don't queue to send, we don't want the chunk -+ return; -+ } -+ -+ final long playerPos = this.broadcastMap.getLastCoordinate(player); -+ final int playerChunkX = CoordinateUtils.getChunkX(playerPos); -+ final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos); -+ final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ); -+ -+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0); -+ data.sendQueue.add(holder); -+ } -+ -+ public void onChunkLoad(final int chunkX, final int chunkZ) { -+ if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ --this.concurrentChunkLoads; -+ } -+ } -+ -+ public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerLoaderData data = this.playerMap.get(player); -+ -+ if (data == null) { -+ return; -+ } -+ -+ data.unloadChunk(chunkX, chunkZ); -+ } -+ -+ public void addPlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot add player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ final PlayerLoaderData data = new PlayerLoaderData(player, this); -+ if (this.playerMap.putIfAbsent(player, data) == null) { -+ data.update(); -+ } -+ } -+ -+ public void removePlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot remove player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ -+ final PlayerLoaderData loaderData = this.playerMap.remove(player); -+ if (loaderData == null) { -+ return; -+ } -+ loaderData.remove(); -+ this.chunkLoadQueue.remove(loaderData); -+ this.chunkSendQueue.remove(loaderData); -+ this.chunkSendWaitQueue.remove(loaderData); -+ synchronized (this.sendingChunkCounts) { -+ final int count = this.sendingChunkCounts.removeInt(loaderData); -+ if (count != 0) { -+ concurrentChunkSends.getAndAdd(-count); -+ } -+ } -+ } -+ -+ public void updatePlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread("Cannot update player async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ final PlayerLoaderData loaderData = this.playerMap.get(player); -+ if (loaderData != null) { -+ loaderData.update(); -+ } -+ } -+ -+ public PlayerLoaderData getData(final ServerPlayer player) { -+ return this.playerMap.get(player); -+ } -+ -+ public void tick() { -+ TickThread.ensureTickThread("Cannot tick async"); -+ for (final PlayerLoaderData data : this.playerMap.values()) { -+ data.update(); -+ } -+ this.tickMidTick(); -+ } -+ -+ protected static final AtomicInteger concurrentChunkSends = new AtomicInteger(); -+ protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>(); -+ private static long nextChunkSend; -+ private void trySendChunks() { -+ final long time = System.nanoTime(); -+ if (time < nextChunkSend) { -+ return; -+ } -+ // drain entries from wait queue -+ while (!this.chunkSendWaitQueue.isEmpty()) { -+ final PlayerLoaderData data = this.chunkSendWaitQueue.first(); -+ -+ if (data.nextChunkSendTarget > time) { -+ break; -+ } -+ -+ this.chunkSendWaitQueue.pollFirst(); -+ -+ this.chunkSendQueue.add(data); -+ } -+ -+ if (this.chunkSendQueue.isEmpty()) { -+ return; -+ } -+ -+ final int maxSends = this.getMaxConcurrentChunkSends(); -+ final long nextPlayerDeadline = this.getTargetSendPerPlayerAddend() + time; -+ for (;;) { -+ if (this.chunkSendQueue.isEmpty()) { -+ break; -+ } -+ final int currSends = concurrentChunkSends.get(); -+ if (currSends >= maxSends) { -+ break; -+ } -+ -+ if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) { -+ continue; -+ } -+ -+ // send chunk -+ -+ final PlayerLoaderData data = this.chunkSendQueue.removeFirst(); -+ -+ final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst(); -+ if (queuedSend == null) { -+ concurrentChunkSends.getAndDecrement(); // we never sent, so decrease -+ // stop iterating over players who have nothing to send -+ if (this.chunkSendQueue.isEmpty()) { -+ // nothing left -+ break; -+ } -+ continue; -+ } -+ -+ if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) { -+ throw new IllegalStateException(); -+ } -+ -+ data.nextChunkSendTarget = nextPlayerDeadline; -+ this.chunkSendWaitQueue.add(data); -+ -+ synchronized (this.sendingChunkCounts) { -+ this.sendingChunkCounts.addTo(data, 1); -+ } -+ -+ data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> { -+ synchronized (this.sendingChunkCounts) { -+ final int count = this.sendingChunkCounts.getInt(data); -+ if (count == 0) { -+ // disconnected, so we don't need to decrement: it will be decremented for us -+ return; -+ } -+ if (count == 1) { -+ this.sendingChunkCounts.removeInt(data); -+ } else { -+ this.sendingChunkCounts.put(data, count - 1); -+ } -+ } -+ -+ concurrentChunkSends.getAndDecrement(); -+ }); -+ -+ nextChunkSend = this.getMaxSendAddend() + time; -+ if (time < nextChunkSend) { -+ break; -+ } -+ } -+ } -+ -+ protected int concurrentChunkLoads; -+ // this interval prevents bursting a lot of chunk loads -+ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_SHORT = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms -+ // this interval ensures the rate is kept between ticks correctly -+ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_LONG = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms -+ private void tryLoadChunks() { -+ if (this.chunkLoadQueue.isEmpty()) { -+ return; -+ } -+ -+ final int maxLoads = this.getMaxChunkLoads(); -+ final long time = System.nanoTime(); -+ boolean updatedCounters = false; -+ for (;;) { -+ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst(); -+ -+ data.lastChunkLoad = time; -+ -+ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst(); -+ if (queuedLoad == null) { -+ if (this.chunkLoadQueue.isEmpty()) { -+ break; -+ } -+ continue; -+ } -+ -+ if (!updatedCounters) { -+ updatedCounters = true; -+ TICKET_ADDITION_COUNTER_SHORT.updateCurrentTime(time); -+ TICKET_ADDITION_COUNTER_LONG.updateCurrentTime(time); -+ data.ticketAdditionCounterShort.updateCurrentTime(time); -+ data.ticketAdditionCounterLong.updateCurrentTime(time); -+ } -+ -+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { -+ // already loaded! -+ data.loadQueue.pollFirst(); // already loaded so we just skip -+ this.chunkLoadQueue.add(data); -+ -+ // ensure the chunk is queued to send -+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); -+ continue; -+ } -+ -+ final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); -+ -+ final double priority = queuedLoad.priority; -+ // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present. -+ // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the -+ // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they -+ // aren't required, it bypasses the limiter system. -+ boolean unloadedTargetChunk = false; -+ unloaded_check: -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = queuedLoad.chunkX + dx; -+ final int offZ = queuedLoad.chunkZ + dz; -+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) { -+ unloadedTargetChunk = true; -+ break unloaded_check; -+ } -+ } -+ } -+ if (unloadedTargetChunk && priority >= 0.0) { -+ // priority >= 0.0 implies rate limited chunks -+ -+ final int currentChunkLoads = this.concurrentChunkLoads; -+ if (currentChunkLoads >= maxLoads || (GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate)) -+ || (GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate > 0.0 && (data.ticketAdditionCounterShort.getRate() >= GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate || data.ticketAdditionCounterLong.getRate() >= GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate))) { -+ // don't poll, we didn't load it -+ this.chunkLoadQueue.add(data); -+ break; -+ } -+ } -+ -+ // can only poll after we decide to load -+ data.loadQueue.pollFirst(); -+ -+ // now that we've polled we can re-add to load queue -+ this.chunkLoadQueue.add(data); -+ -+ // add necessary tickets to load chunk up to send-ready -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = queuedLoad.chunkX + dx; -+ final int offZ = queuedLoad.chunkZ + dz; -+ final ChunkPos chunkPos = new ChunkPos(offX, offZ); -+ -+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); -+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) { -+ continue; -+ } -+ -+ if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) { -+ // won't reach here if unloadedTargetChunk is false -+ ++this.concurrentChunkLoads; -+ TICKET_ADDITION_COUNTER_SHORT.addTime(time); -+ TICKET_ADDITION_COUNTER_LONG.addTime(time); -+ data.ticketAdditionCounterShort.addTime(time); -+ data.ticketAdditionCounterLong.addTime(time); -+ } -+ } -+ } -+ -+ // mark that we've added tickets here -+ this.isTargetedForPlayerLoad.add(chunkKey); -+ -+ // it's possible all we needed was the player tickets to queue up the send. -+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { -+ // yup, all we needed. -+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); -+ } else if (this.chunkNeedsPostProcessing(queuedLoad.chunkX, queuedLoad.chunkZ)) { -+ // requires post processing -+ this.chunkMap.mainThreadExecutor.execute(() -> { -+ final long key = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); -+ final ChunkHolder holder = PlayerChunkLoader.this.chunkMap.getVisibleChunkIfPresent(key); -+ -+ if (holder == null) { -+ return; -+ } -+ -+ final LevelChunk chunk = holder.getSendingChunk(); -+ -+ if (chunk != null && !chunk.isPostProcessingDone) { -+ chunk.postProcessGeneration(); -+ } -+ }); -+ } -+ } -+ } -+ -+ public void tickMidTick() { -+ // try to send more chunks -+ this.trySendChunks(); -+ -+ // try to queue more chunks to load -+ this.tryLoadChunks(); -+ } -+ -+ static final class ChunkPriorityHolder { -+ public final int chunkX; -+ public final int chunkZ; -+ public final int manhattanDistanceToPlayer; -+ public final double priority; -+ -+ public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) { -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.manhattanDistanceToPlayer = manhattanDistanceToPlayer; -+ this.priority = priority; -+ } -+ } -+ -+ public static final class PlayerLoaderData { -+ -+ protected static final float FOV = 110.0f; -+ protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0; -+ -+ // Player max sprint speed is approximately 8m/s -+ protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0); -+ protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f; -+ -+ protected double lastLocX = Double.NEGATIVE_INFINITY; -+ protected double lastLocZ = Double.NEGATIVE_INFINITY; -+ -+ protected int lastChunkX = Integer.MIN_VALUE; -+ protected int lastChunkZ = Integer.MIN_VALUE; -+ -+ // this is corrected so that 0 is along the positive x-axis -+ protected float lastYaw = Float.NEGATIVE_INFINITY; -+ -+ protected int lastSendDistance = Integer.MIN_VALUE; -+ protected int lastLoadDistance = Integer.MIN_VALUE; -+ protected int lastTickDistance = Integer.MIN_VALUE; -+ protected boolean usingLookingPriority; -+ -+ protected final ServerPlayer player; -+ protected final PlayerChunkLoader loader; -+ -+ // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field -+ // in a comparator! -+ protected final ArrayDeque loadQueue = new ArrayDeque<>(); -+ protected final LongOpenHashSet sentChunks = new LongOpenHashSet(); -+ protected final LongOpenHashSet chunksToBeSent = new LongOpenHashSet(); -+ -+ protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { -+ final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer); -+ if (distanceCompare != 0) { -+ return distanceCompare; -+ } -+ -+ final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX); -+ if (coordinateXCompare != 0) { -+ return coordinateXCompare; -+ } -+ -+ return Integer.compare(p1.chunkZ, p2.chunkZ); -+ }); -+ -+ protected int sendViewDistance = -1; -+ protected int loadViewDistance = -1; -+ protected int tickViewDistance = -1; -+ -+ protected long nextChunkSendTarget; -+ -+ // this interval prevents bursting a lot of chunk loads -+ protected final IntervalledCounter ticketAdditionCounterShort = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms -+ // this ensures the rate is kept between ticks correctly -+ protected final IntervalledCounter ticketAdditionCounterLong = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms -+ -+ public long lastChunkLoad; -+ -+ public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) { -+ this.player = player; -+ this.loader = loader; -+ } -+ -+ // these view distance methods are for api -+ public int getTargetSendViewDistance() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ final int clientViewDistance = this.getClientViewDistance(); -+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!GlobalConfiguration.get().chunkLoading.autoconfigSendDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); -+ return sendViewDistance; -+ } -+ -+ public void setTargetSendViewDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Send view distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); -+ } -+ this.sendViewDistance = distance; -+ } -+ -+ public int getTargetNoTickViewDistance() { -+ return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1; -+ } -+ -+ public void setTargetNoTickViewDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { -+ throw new IllegalArgumentException("Simulation distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + " or -1, got: " + distance); -+ } -+ this.loadViewDistance = distance == -1 ? -1 : distance + 1; -+ } -+ -+ public int getTargetTickViewDistance() { -+ return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ } -+ -+ public void setTargetTickViewDistance(final int distance) { -+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { -+ throw new IllegalArgumentException("View distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + " or -1, got: " + distance); -+ } -+ this.tickViewDistance = distance; -+ } -+ -+ protected int getLoadDistance() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ -+ return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ } -+ -+ public boolean hasSentChunk(final int chunkX, final int chunkZ) { -+ return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) { -+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, -+ new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded -+ this.player.connection.connection.execute(onChunkSend); -+ } else { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ public void unloadChunk(final int chunkX, final int chunkZ) { -+ if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, -+ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded -+ } -+ } -+ -+ protected static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, -+ final int sendRadius) { -+ // expect sendRadius to be = 1 + target viewable radius -+ return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius); -+ } -+ -+ protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point -+ final double p2x, final double p2z, // triangle point -+ final double p3x, final double p3z, // triangle point -+ -+ final double targetX, final double targetZ) { // point -+ // from barycentric coordinates: -+ // targetX = a*p1x + b*p2x + c*p3x -+ // targetZ = a*p1z + b*p2z + c*p3z -+ // 1.0 = a*1.0 + b*1.0 + c*1.0 -+ // where a, b, c >= 0.0 -+ // so, if any of a, b, c are less-than zero then there is no intersection. -+ -+ // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z)) -+ // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d -+ // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d -+ // c = 1.0 - a - b -+ -+ final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z); -+ final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d; -+ -+ if (a < 0.0 || a > 1.0) { -+ return false; -+ } -+ -+ final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d; -+ if (b < 0.0 || b > 1.0) { -+ return false; -+ } -+ -+ final double c = 1.0 - a - b; -+ -+ return c >= 0.0 && c <= 1.0; -+ } -+ -+ public void remove() { -+ this.loader.broadcastMap.remove(this.player); -+ this.loader.loadMap.remove(this.player); -+ this.loader.loadTicketCleanup.remove(this.player); -+ this.loader.tickMap.remove(this.player); -+ } -+ -+ protected int getClientViewDistance() { -+ return this.player.clientViewDistance == null ? -1 : Math.max(0, this.player.clientViewDistance.intValue()); -+ } -+ -+ public void update() { -+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; -+ // load view cannot be less-than tick view + 1 -+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); -+ // send view cannot be greater-than load view -+ final int clientViewDistance = this.getClientViewDistance(); -+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!GlobalConfiguration.get().chunkLoading.autoconfigSendDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); -+ -+ final double posX = this.player.getX(); -+ final double posZ = this.player.getZ(); -+ final float yaw = MCUtil.normalizeYaw(this.player.yRot + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis -+ -+ // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them. -+ final boolean useLookPriority = GlobalConfiguration.get().chunkLoading.enableFrustumPriority && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD || -+ this.player.getAbilities().flying); -+ -+ // make sure we're in the send queue -+ this.loader.chunkSendWaitQueue.add(this); -+ -+ if ( -+ // has view distance stayed the same? -+ sendViewDistance == this.lastSendDistance -+ && loadViewDistance == this.lastLoadDistance -+ && tickViewDistance == this.lastTickDistance -+ -+ && (this.usingLookingPriority ? ( -+ // has our block stayed the same (this also accounts for chunk change)? -+ Mth.floor(this.lastLocX) == Mth.floor(posX) -+ && Mth.floor(this.lastLocZ) == Mth.floor(posZ) -+ ) : ( -+ // has our chunk stayed the same -+ (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4) -+ && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4) -+ )) -+ -+ // has our decision about look priority changed? -+ && this.usingLookingPriority == useLookPriority -+ -+ // if we are currently using look priority, has our yaw stayed within recalc threshold? -+ && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD) -+ ) { -+ // nothing we care about changed, so we're not re-calculating -+ return; -+ } -+ -+ final int centerChunkX = Mth.floor(posX) >> 4; -+ final int centerChunkZ = Mth.floor(posZ) >> 4; -+ -+ final boolean needsChunkCenterUpdate = (centerChunkX != this.lastChunkX) || (centerChunkZ != this.lastChunkZ); -+ this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance); -+ this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance); -+ this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1); -+ this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance); -+ -+ if (sendViewDistance != this.lastSendDistance) { -+ // update the view radius for client -+ // note that this should be after the map calls because the client wont expect unload calls not in its VD -+ // and it's possible we decreased VD here -+ this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance)); -+ } -+ if (tickViewDistance != this.lastTickDistance) { -+ this.player.connection.send(new ClientboundSetSimulationDistancePacket(tickViewDistance)); -+ } -+ -+ this.lastLocX = posX; -+ this.lastLocZ = posZ; -+ this.lastYaw = yaw; -+ this.lastSendDistance = sendViewDistance; -+ this.lastLoadDistance = loadViewDistance; -+ this.lastTickDistance = tickViewDistance; -+ this.usingLookingPriority = useLookPriority; -+ -+ this.lastChunkX = centerChunkX; -+ this.lastChunkZ = centerChunkZ; -+ -+ // points for player "view" triangle: -+ -+ // obviously, the player pos is a vertex -+ final double p1x = posX; -+ final double p1z = posZ; -+ -+ // to the left of the looking direction -+ final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector -+ + p1x; // offset vector -+ final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector -+ + p1z; // offset vector -+ -+ // to the right of the looking direction -+ final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector -+ + p1x; // offset vector -+ final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector -+ + p1z; // offset vector -+ -+ // now that we have all of our points, we can recalculate the load queue -+ -+ final List loadQueue = new ArrayList<>(); -+ -+ // clear send queue, we are re-sorting -+ this.sendQueue.clear(); -+ // clear chunk want set, vd/position might have changed -+ this.chunksToBeSent.clear(); -+ -+ final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance); -+ -+ for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) { -+ for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) { -+ final int chunkX = dx + centerChunkX; -+ final int chunkZ = dz + centerChunkZ; -+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); -+ final boolean sendChunk = squareDistance <= sendViewDistance && wantChunkLoaded(centerChunkX, centerChunkZ, chunkX, chunkZ, sendViewDistance); -+ -+ if (this.hasSentChunk(chunkX, chunkZ)) { -+ // already sent (which means it is also loaded) -+ if (!sendChunk) { -+ // have sent the chunk, but don't want it anymore -+ // unload it now -+ this.unloadChunk(chunkX, chunkZ); -+ } -+ continue; -+ } -+ -+ final boolean loadChunk = squareDistance <= loadViewDistance; -+ -+ final boolean prioritised = useLookPriority && triangleIntersects( -+ // prioritisation triangle -+ p1x, p1z, p2x, p2z, p3x, p3z, -+ -+ // center of chunk -+ (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8) -+ ); -+ -+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); -+ -+ final double priority; -+ -+ if (squareDistance <= GlobalConfiguration.get().chunkLoading.minLoadRadius) { -+ // priority should be negative, and we also want to order it from center outwards -+ // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest -+ priority = -((2 * GlobalConfiguration.get().chunkLoading.minLoadRadius + 1) - manhattanDistance); -+ } else { -+ if (prioritised) { -+ // we don't prioritise these chunks above others because we also want to make sure some chunks -+ // will be loaded if the player changes direction -+ priority = (double)manhattanDistance / 6.0; -+ } else { -+ priority = (double)manhattanDistance; -+ } -+ } -+ -+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority); -+ -+ if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) { -+ if (loadChunk) { -+ loadQueue.add(holder); -+ if (sendChunk) { -+ this.chunksToBeSent.add(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ } -+ } else { -+ // loaded but not sent: so queue it! -+ if (sendChunk) { -+ this.sendQueue.add(holder); -+ } -+ } -+ } -+ } -+ -+ loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { -+ return Double.compare(p1.priority, p2.priority); -+ }); -+ -+ // we're modifying loadQueue, must remove -+ this.loader.chunkLoadQueue.remove(this); -+ -+ this.loadQueue.clear(); -+ this.loadQueue.addAll(loadQueue); -+ -+ // must re-add -+ this.loader.chunkLoadQueue.add(this); -+ -+ // update the chunk center -+ // this must be done last so that the client does not ignore any of our unload chunk packets -+ if (needsChunkCenterUpdate) { -+ this.player.connection.send(new ClientboundSetChunkCacheCenterPacket(centerChunkX, centerChunkZ)); -+ } -+ } -+ } -+} -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- 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> { - public boolean queueImmunity = false; - public ConnectionProtocol protocol; - // Paper end -+ // Paper start - add pending task queue -+ private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ public void execute(final Runnable run) { -+ if (this.channel == null || !this.channel.isRegistered()) { -+ run.run(); -+ return; -+ } -+ final boolean queue = !this.queue.isEmpty(); -+ if (!queue) { -+ this.channel.eventLoop().execute(run); -+ } else { -+ this.pendingTasks.add(run); -+ if (this.queue.isEmpty()) { -+ // something flushed async, dump tasks now -+ Runnable r; -+ while ((r = this.pendingTasks.poll()) != null) { -+ this.channel.eventLoop().execute(r); -+ } -+ } -+ } -+ } -+ // Paper end - add pending task queue - - // Paper start - allow controlled flushing - volatile boolean canFlush = true; -@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { - return false; - } - 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; -@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { - } - } - return true; -+ } finally { // Paper start - add pending task queue -+ Runnable r; -+ while ((r = this.pendingTasks.poll()) != null) { -+ this.channel.eventLoop().execute(r); -+ } -+ } // Paper end - add pending task queue - } - // Paper end - -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 @@ public final class MCUtil { - }); - - worldData.addProperty("name", world.getWorld().getName()); -- worldData.addProperty("view-distance", world.spigotConfig.viewDistance); -+ worldData.addProperty("view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace chunk loader system -+ worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system - worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory); - worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16); - worldData.addProperty("visible-chunk-count", allChunks.size()); -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 ServerLevel getWorld() { return chunkMap.level; } // Paper - boolean isUpdateQueued = false; // Paper - private final ChunkMap chunkMap; // Paper -+ // Paper start - no-tick view distance -+ public final LevelChunk getSendingChunk() { -+ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used -+ // in Chunk's neighbour callback -+ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z); -+ if (ret != null && ret.areNeighboursLoaded(1)) { -+ return ret; -+ } -+ return null; -+ } -+ // Paper end - no-tick view distance - - // Paper start - public void onChunkAdd() { -@@ -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(); -+ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance - - if (chunk != null) { - int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); -@@ -0,0 +0,0 @@ public class ChunkHolder { - } - - public void sectionLightChanged(LightLayer lightType, int y) { -- Either either = (Either) this.getFutureIfPresent(ChunkStatus.FEATURES).getNow(null); // CraftBukkit - decompile error -+ // Paper start - no-tick view distance - -- if (either != null) { -- ChunkAccess ichunkaccess = (ChunkAccess) either.left().orElse(null); // CraftBukkit - decompile error -+ if (true) { -+ ChunkAccess ichunkaccess = this.getAvailableChunkNow(); - - if (ichunkaccess != null) { - ichunkaccess.setUnsaved(true); -- LevelChunk chunk = this.getTickingChunk(); -+ LevelChunk chunk = this.getSendingChunk(); -+ // Paper end - no-tick view distance - - if (chunk != null) { - int j = this.lightEngine.getMinLightSection(); -@@ -0,0 +0,0 @@ public class ChunkHolder { - } - - public void broadcast(Packet packet, boolean onlyOnWatchDistanceEdge) { -- this.playerProvider.getPlayers(this.pos, onlyOnWatchDistanceEdge).forEach((entityplayer) -> { -- entityplayer.connection.send(packet); -- }); -+ // Paper start - per player view distance -+ // there can be potential desync with player's last mapped section and the view distance map, so use the -+ // view distance map here. -+ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); -+ if (players == null) { -+ return; -+ } -+ -+ Object[] backingSet = players.getBackingSet(); -+ for (int i = 0, len = backingSet.length; i < len; ++i) { -+ Object temp = backingSet[i]; -+ if (!(temp instanceof ServerPlayer)) { -+ continue; -+ } -+ ServerPlayer player = (ServerPlayer)temp; -+ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { -+ continue; -+ } -+ player.connection.send(packet); -+ } -+ // Paper end - per player view distance - } - - public CompletableFuture> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) { -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 - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; - // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning -+ public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader - // Paper start - use distance map to optimise tracker - public static boolean isLegacyTrackingEntity(Entity entity) { - return entity.isLegacyTrackingEntity; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - use distance map to optimise tracker - - void addPlayerToDistanceMaps(ServerPlayer player) { -+ this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader - int chunkX = MCUtil.getChunkCoordinate(player.getX()); - int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); - // Paper start - use distance map to optimise entity tracker -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - -- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); -+ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player))); // Paper - per player view distances - } - // Paper end - use distance map to optimise entity tracker - // Note: players need to be explicitly added to distance maps before they can be updated -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerMobDistanceMap.remove(player); - } - // Paper end - per player mob spawning -+ this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader - } - - void updateMaps(ServerPlayer player) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - -- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); -+ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player))); // Paper - per player view distances - } - // Paper end - use distance map to optimise entity tracker - this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerMobDistanceMap.update(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); - } - // Paper end - per player mob spawning -+ this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader - } - // Paper end - // Paper start -@@ -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<>(); // Paper - Anti-Xray - Bypass -- -- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> { -- this.playerLoadedChunk(entityplayer, mutableobject, chunk); -- }); -+ // Paper - no-tick view distance - moved to Chunk neighbour update - }); - }, (runnable) -> { - this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int k = this.viewDistance; - - this.viewDistance = j; -- this.distanceManager.updatePlayerTickets(this.viewDistance + 1); -- Iterator objectiterator = net.minecraft.server.ChunkSystem.getUpdatingChunkHolders(this.level).iterator(); // Paper -- -- while (objectiterator.hasNext()) { -- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next(); -- ChunkPos chunkcoordintpair = playerchunk.getPos(); -- MutableObject> mutableobject = new MutableObject<>(); // Paper - Anti-Xray - Bypass -- -- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> { -- SectionPos sectionposition = entityplayer.getLastSectionPos(); -- boolean flag = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), k); -- boolean flag1 = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), this.viewDistance); -- -- this.updateChunkTracking(entityplayer, chunkcoordintpair, mutableobject, flag, flag1); -- }); -- } -+ this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system - } - - } - -- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - Anti-Xray - Bypass -+ // Paper start - replace player loader system -+ public void setTickViewDistance(int distance) { -+ this.playerChunkManager.setTickDistance(distance); -+ } -+ // Paper end - replace player loader system -+ -+ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - Anti-Xray - Bypass // Paper - public - if (player.level == this.level) { - if (newWithinViewDistance && !oldWithinViewDistance) { - ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); - - if (playerchunk != null) { -- LevelChunk chunk = playerchunk.getTickingChunk(); -+ LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system - - if (chunk != null) { - this.playerLoadedChunk(player, packet, chunk); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - void dumpChunks(Writer writer) throws IOException { - CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); -- TickingTracker tickingtracker = this.distanceManager.tickingTracker(); -+ // Paper - replace loader system - Iterator objectbidirectionaliterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper - - while (objectbidirectionaliterator.hasNext()) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // CraftBukkit - decompile error - csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> { - return chunk.getBlockEntities().size(); -- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> { -+ }).orElse(0), "Use ticket level", -1000, optional1.map((chunk) -> { // Paper - replace loader system - return chunk.getBlockTicks().count(); - }).orElse(0), optional1.map((chunk) -> { - return chunk.getFluidTicks().count(); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.removePlayerFromDistanceMaps(player); // Paper - distance maps - } - -- for (int k = i - this.viewDistance - 1; k <= i + this.viewDistance + 1; ++k) { -- for (int l = j - this.viewDistance - 1; l <= j + this.viewDistance + 1; ++l) { -- if (ChunkMap.isChunkInRange(k, l, i, j, this.viewDistance)) { -- ChunkPos chunkcoordintpair = new ChunkPos(k, l); -- -- this.updateChunkTracking(player, chunkcoordintpair, new MutableObject(), !added, added); -- } -- } -- } -+ // Paper - handled by player chunk loader - - } - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - SectionPos sectionposition = SectionPos.of((EntityAccess) player); - - player.setLastSectionPos(sectionposition); -- player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); -+ //player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); // Paper - handled by player chunk loader - return sectionposition; - } - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int k1; - int l1; - -- if (Math.abs(i1 - i) <= this.viewDistance * 2 && Math.abs(j1 - j) <= this.viewDistance * 2) { -- k1 = Math.min(i, i1) - this.viewDistance - 1; -- l1 = Math.min(j, j1) - this.viewDistance - 1; -- int i2 = Math.max(i, i1) + this.viewDistance + 1; -- int j2 = Math.max(j, j1) + this.viewDistance + 1; -- -- for (int k2 = k1; k2 <= i2; ++k2) { -- for (int l2 = l1; l2 <= j2; ++l2) { -- boolean flag3 = ChunkMap.isChunkInRange(k2, l2, i1, j1, this.viewDistance); -- boolean flag4 = ChunkMap.isChunkInRange(k2, l2, i, j, this.viewDistance); -- -- this.updateChunkTracking(player, new ChunkPos(k2, l2), new MutableObject(), flag3, flag4); -- } -- } -- } else { -- boolean flag5; -- boolean flag6; -- -- for (k1 = i1 - this.viewDistance - 1; k1 <= i1 + this.viewDistance + 1; ++k1) { -- for (l1 = j1 - this.viewDistance - 1; l1 <= j1 + this.viewDistance + 1; ++l1) { -- if (ChunkMap.isChunkInRange(k1, l1, i1, j1, this.viewDistance)) { -- flag5 = true; -- flag6 = false; -- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), true, false); -- } -- } -- } -- -- for (k1 = i - this.viewDistance - 1; k1 <= i + this.viewDistance + 1; ++k1) { -- for (l1 = j - this.viewDistance - 1; l1 <= j + this.viewDistance + 1; ++l1) { -- if (ChunkMap.isChunkInRange(k1, l1, i, j, this.viewDistance)) { -- flag5 = false; -- flag6 = true; -- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), false, true); -- } -- } -- } -- } -+ // Paper - replaced by PlayerChunkLoader - - this.updateMaps(player); // Paper - distance maps -+ this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately - - } - - @Override - public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { -- Set set = this.playerMap.getPlayers(chunkPos.toLong()); -- Builder builder = ImmutableList.builder(); -- Iterator iterator = set.iterator(); -+ // Paper start - per player view distance -+ // there can be potential desync with player's last mapped section and the view distance map, so use the -+ // view distance map here. -+ List ret = new java.util.ArrayList<>(4); - -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- SectionPos sectionposition = entityplayer.getLastSectionPos(); -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); -+ if (players == null) { -+ return ret; -+ } - -- if (onlyOnWatchDistanceEdge && ChunkMap.isChunkOnRangeBorder(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance) || !onlyOnWatchDistanceEdge && ChunkMap.isChunkInRange(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance)) { -- builder.add(entityplayer); -+ Object[] backingSet = players.getBackingSet(); -+ for (int i = 0, len = backingSet.length; i < len; ++i) { -+ Object temp = backingSet[i]; -+ if (!(temp instanceof ServerPlayer)) { -+ continue; -+ } -+ ServerPlayer player = (ServerPlayer)temp; -+ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) { -+ continue; - } -+ ret.add(player); - } - -- return builder.build(); -+ return ret; -+ // Paper end - per player view distance - } - - public void addEntity(Entity entity) { -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - double vec3d_dx = player.getX() - this.entity.getX(); - double vec3d_dz = player.getZ() - this.entity.getZ(); - // Paper end - remove allocation of Vec3D here -- double d0 = (double) Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); -+ double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance - double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper - double d2 = d0 * d0; - boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index 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 final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); - //private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Paper - replace ticket level propagator - public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used -- private final TickingTracker tickingTicketsTracker = new TickingTracker(); -- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); -+ //private final TickingTracker tickingTicketsTracker = new TickingTracker(); // Paper - no longer used -+ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used - // Paper start use a queue, but still keep unique requirement - public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() { - @Override -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - java.util.function.Predicate> removeIf = (ticket) -> { - final boolean ret = ticket.timedOut(ticketCounter); - if (ret) { -- this.tickingTicketsTracker.removeTicket(currChunk[0], ticket); -+ //this.tickingTicketsTracker.removeTicket(currChunk[0], ticket); // Paper - no longer used - } - return ret; - }; -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - if (ticket.timedOut(this.ticketTickCounter)) { - iterator.remove(); - flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -+ //this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); // Paper - no longer used - } - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - protected long ticketLevelUpdateCount; // Paper - replace ticket level propagator - public boolean runAllUpdates(ChunkMap chunkStorage) { - //this.f.a(); // Paper - no longer used -- this.tickingTicketsTracker.runAllUpdates(); -+ //this.tickingTicketsTracker.runAllUpdates(); // Paper - no longer used - org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper -- this.playerTicketManager.runAllUpdates(); -+ // this.playerTicketManager.runAllUpdates(); // Paper - no longer used - boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Paper - replace ticket level propagator - - if (flag) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - long j = chunkcoordintpair.toLong(); - - boolean added = this.addTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.addTicket(j, ticket); -+ //this.tickingTicketsTracker.addTicket(j, ticket); // Paper - no longer used - return added; // CraftBukkit - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - long j = chunkcoordintpair.toLong(); - - boolean removed = this.removeTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.removeTicket(j, ticket); -+ //this.tickingTicketsTracker.removeTicket(j, ticket); // Paper - no longer used - return removed; // CraftBukkit - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - - if (forced) { - this.addTicket(i, ticket); -- this.tickingTicketsTracker.addTicket(i, ticket); -+ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used - } else { - this.removeTicket(i, ticket); -- this.tickingTicketsTracker.removeTicket(i, ticket); -+ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used - } - - } -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - return new ObjectOpenHashSet(); - })).add(player); - //this.f.update(i, 0, true); // Paper - no longer used -- this.playerTicketManager.update(i, 0, true); -- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); -+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used -+ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } - - public void removePlayer(SectionPos pos, ServerPlayer player) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - if (objectset == null || objectset.isEmpty()) { // Paper - this.playersPerChunk.remove(i); - //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used -- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); -- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); -+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used -+ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } - - } -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - public boolean inEntityTickingRange(long chunkPos) { -- return this.tickingTicketsTracker.getLevel(chunkPos) < 32; -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isEntityTickingReady(); -+ // Paper end - replace player chunk loader system - } - - public boolean inBlockTickingRange(long chunkPos) { -- return this.tickingTicketsTracker.getLevel(chunkPos) < 33; -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - protected String getTicketDebugString(long pos) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - protected void updatePlayerTickets(int viewDistance) { -- this.playerTicketManager.updateViewDistance(viewDistance); -+ this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager - } - - public void updateSimulationDistance(int simulationDistance) { -- if (simulationDistance != this.simulationDistance) { -- this.simulationDistance = simulationDistance; -- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); -- } -- -+ this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager - } - - // Paper start - public int getSimulationDistance() { -- return this.simulationDistance; -+ return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager - } - // Paper end - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - - } - -- @VisibleForTesting -- TickingTracker tickingTracker() { -- return this.tickingTicketsTracker; -- } -+ // Paper - replace player chunk loader - - public void removeTicketsOnClosing() { - ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.ASYNC_LOAD, TicketType.REQUIRED_LOAD, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - if (!immutableset.contains(ticket.getType())) { - iterator.remove(); - flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -+ // this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); // Paper - no longer used - } - } - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - // CraftBukkit end - -+ /* Paper - replace old loader system - private class ChunkTicketTracker extends ChunkTracker { - - public ChunkTicketTracker() { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - return distance <= this.viewDistance - 2; - } - } -+ */ // Paper - replace old loader system - } -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 { - // Paper end - - public boolean isPositionTicking(long pos) { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -- -- if (playerchunk == null) { -- return false; -- } else if (!this.level.shouldTickBlocksAt(pos)) { -- return false; -- } else { -- Either either = (Either) playerchunk.getTickingChunkFuture().getNow(null); // CraftBukkit - decompile error -- -- return either != null && either.left().isPresent(); -- } -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - public void save(boolean flush) { -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - this.level.getProfiler().popPush("chunks"); - if (tickChunks) { - this.level.timings.chunks.startTiming(); // Paper - timings -+ this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes - this.tickChunks(); - this.level.timings.chunks.stopTiming(); // Paper - timings - } -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - // Paper end - optimise chunk tick iteration - ChunkPos chunkcoordintpair = chunk1.getPos(); - -- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning -+ if ((true || this.level.isNaturalSpawningAllowed(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning // Paper - replace player chunk loader system - chunk1.incrementInhabitedTime(j); - if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration - NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); - } - -- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { -+ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - replace player chunk loader system - this.level.tickChunk(chunk1, k); - if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper - } -@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { - public boolean pollTask() { - try { - boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper -+ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Paper - if (ServerChunkCache.this.runDistanceManagerUpdates()) { - return true; - } else { -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 { - gameprofilerfiller.push("checkDespawn"); - entity.checkDespawn(); - gameprofilerfiller.pop(); -- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { -+ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list - Entity entity1 = entity.getVehicle(); - - if (entity1 != null) { -@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { - - @Override - public boolean shouldTickBlocksAt(long chunkPos) { -- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos); -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - protected void tickTime() { -@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { - private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { - // Paper start - optimize is ticking ready type functions - ChunkHolder chunkHolder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); -- return chunkHolder != null && this.chunkSource.isPositionTicking(chunkPos) && chunkHolder.isTickingReady() && this.areEntitiesLoaded(chunkPos); -+ return chunkHolder != null && chunkHolder.isTickingReady() && this.areEntitiesLoaded(chunkPos); // Paper - no longer need to check with chunk source - // Paper end - } - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- 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 { - } - // CraftBukkit end - -- public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder -+ public final int getViewDistance() { throw new UnsupportedOperationException("Use PlayerChunkLoader"); } // Paper - placeholder - } -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -0,0 +0,0 @@ public abstract class PlayerList { - boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); - - // Spigot - view distance -- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management - player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit - playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); - playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); -@@ -0,0 +0,0 @@ public abstract class PlayerList { - // CraftBukkit start - LevelData worlddata = worldserver1.getLevelData(); - entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag, entityplayer1.getLastDeathLocation())); -- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot -- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot -+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management -+ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management - entityplayer1.spawnIn(worldserver1); - entityplayer1.unsetRemoved(); - entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); -@@ -0,0 +0,0 @@ public abstract class PlayerList { - - public void setViewDistance(int viewDistance) { - this.viewDistance = viewDistance; -- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); -+ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { -@@ -0,0 +0,0 @@ public abstract class PlayerList { - - public void setSimulationDistance(int simulationDistance) { - this.simulationDistance = simulationDistance; -- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); -+ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java -+++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java -@@ -0,0 +0,0 @@ public class EnderDragon extends Mob implements Enemy { - // this.world.b(1028, this.getChunkCoordinates(), 0); - //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API - for (net.minecraft.server.level.ServerPlayer player : (List) ((ServerLevel)level).players()) { -- final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch -+ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - route to player chunk loader - double deltaX = this.getX() - player.getX(); - double deltaZ = this.getZ() - player.getZ(); - double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; -diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -+++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -@@ -0,0 +0,0 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob - // this.world.globalLevelEvent(1023, new BlockPosition(this), 0); - //int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API - for (ServerPlayer player : (List)this.level.players()) { // Paper -- final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch -+ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - route to player chunk loader - double deltaX = this.getX() - player.getX(); - double deltaZ = this.getZ() - player.getZ(); - double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; -diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java -+++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java -@@ -0,0 +0,0 @@ public class EnderEyeItem extends Item { - - // CraftBukkit start - Use relative location for far away sounds - // world.b(1038, blockposition1.c(1, 0, 1), 0); -- int viewDistance = world.getCraftServer().getViewDistance() * 16; -+ //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Paper - apply view distance patch - BlockPos soundPos = blockposition1.offset(1, 0, 1); - for (ServerPlayer player : world.getServer().getPlayerList().players) { -+ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - apply view distance patch - double deltaX = soundPos.getX() - player.getX(); - double deltaZ = soundPos.getZ() - player.getZ(); - double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; -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 { - - if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement - this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); -+ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance -+ // if copied from above -+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management -+ ((ServerLevel)this).getChunkSource().blockChanged(blockposition); -+ // Paper end - per player view distance - } - - if ((i & 1) != 0) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - - protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { - -+ // Paper start - no-tick view distance -+ ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource(); -+ net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap; -+ // this code handles the addition of ticking tickets - the distance map handles the removal -+ if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { -+ if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system -+ // now we're ready for entity ticking -+ chunkProviderServer.mainThreadProcessor.execute(() -> { -+ // double check that this condition still holds. -+ if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system -+ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk -+ chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update -+ } -+ }); -+ } -+ } -+ -+ // this code handles the chunk sending -+ if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { -+ // Paper start - replace old player chunk loading system -+ if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) { -+ // the post processing is expensive, so we don't want to run it unless we're actually near -+ // a player. -+ chunkProviderServer.mainThreadProcessor.execute(() -> { -+ if (!LevelChunk.this.areNeighboursLoaded(1)) { -+ return; -+ } -+ LevelChunk.this.postProcessGeneration(); -+ if (!LevelChunk.this.areNeighboursLoaded(1)) { -+ return; -+ } -+ chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); -+ }); -+ } -+ // Paper end - replace old player chunk loading system -+ } -+ // Paper end - no-tick view distance - } - - public final boolean isAnyNeighborsLoaded() { -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - // Paper end - neighbour cache - org.bukkit.Server server = this.level.getCraftServer(); - this.level.getChunkSource().addLoadedChunk(this); // Paper -+ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management - if (server != null) { - /* - * If it's a new world, the first few chunks are generated inside -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - }); - } - -+ public boolean isPostProcessingDone; // Paper - replace chunk loader system -+ - public void postProcessGeneration() { -+ try { // Paper - replace chunk loader system - ChunkPos chunkcoordintpair = this.getPos(); - - for (int i = 0; i < this.postProcessing.length; ++i) { -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - - this.pendingBlockEntities.clear(); - this.upgradeData.upgrade(this); -+ } finally { // Paper start - replace chunk loader system -+ this.isPostProcessingDone = true; -+ this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z); -+ } -+ // Paper end - replace chunk loader system - } - - @Nullable -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); - if (playerChunk == null) return false; - -- playerChunk.getTickingChunkFuture().thenAccept(either -> { -- either.left().ifPresent(chunk -> { -+ // Paper start - rewrite player chunk loader -+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getSendingChunk(); -+ if (chunk == null) { -+ return false; -+ } -+ // Paper end - rewrite player chunk loader - List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); -- if (playersInRange.isEmpty()) return; -+ if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader - - // Paper start - Anti-Xray - Bypass - Map refreshPackets = new HashMap<>(); -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - })); - // Paper end - } -- }); -- }); -+ // Paper - rewrite player chunk loader - - return true; - } -@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { - // Spigot start - @Override - public int getViewDistance() { -- return world.spigotConfig.viewDistance; -+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management - } - - @Override - public int getSimulationDistance() { -- return world.spigotConfig.simulationDistance; -+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - replace old player chunk management - } - // Spigot end - // Paper start - view distance api - @Override - public void setViewDistance(int viewDistance) { -- throw new UnsupportedOperationException(); //TODO -+ // Paper start - replace old player chunk management -+ if (viewDistance < 2 || viewDistance > 32) { -+ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); -+ } -+ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; -+ chunkMap.setViewDistance(viewDistance); -+ // Paper end - replace old player chunk management - } - -+ // Paper start - replace old player chunk management - @Override - public void setSimulationDistance(int simulationDistance) { -- throw new UnsupportedOperationException(); //TODO -+ // Paper start - replace old player chunk management -+ if (simulationDistance < 2 || simulationDistance > 32) { -+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]"); -+ } -+ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; -+ chunkMap.setTickViewDistance(simulationDistance); - } -+ // Paper end - replace old player chunk management - - @Override - public int getNoTickViewDistance() { -- throw new UnsupportedOperationException(); //TODO -+ return this.getViewDistance(); // Paper - replace old player chunk management - } - - @Override - public void setNoTickViewDistance(int viewDistance) { -- throw new UnsupportedOperationException(); //TODO -+ this.setViewDistance(viewDistance); // Paper - replace old player chunk management - } - - @Override - public int getSendViewDistance() { -- throw new UnsupportedOperationException(); //TODO -+ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); // Paper - replace old player chunk management - } - - @Override - public void setSendViewDistance(int viewDistance) { -- throw new UnsupportedOperationException(); //TODO -+ getHandle().getChunkSource().chunkMap.playerChunkManager.setSendDistance(viewDistance); // Paper - replace old player chunk management - } - // Paper end - view distance api - -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - } - } - -+ // Paper start - implement view distances - @Override - public int getViewDistance() { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); -+ } -+ return data.getTargetNoTickViewDistance(); - } - - @Override - public void setViewDistance(int viewDistance) { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetNoTickViewDistance(viewDistance); - } - - @Override - public int getSimulationDistance() { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetTickViewDistance(); -+ } -+ return data.getTargetTickViewDistance(); - } - - @Override - public void setSimulationDistance(int simulationDistance) { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetTickViewDistance(simulationDistance); - } - - @Override - public int getNoTickViewDistance() { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ return this.getViewDistance(); - } - - @Override - public void setNoTickViewDistance(int viewDistance) { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ this.setViewDistance(viewDistance); - } - - @Override - public int getSendViewDistance() { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ return chunkMap.playerChunkManager.getTargetSendDistance(); -+ } -+ return data.getTargetSendViewDistance(); - } - - @Override - public void setSendViewDistance(int viewDistance) { -- throw new UnsupportedOperationException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO -+ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; -+ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); -+ if (data == null) { -+ throw new IllegalStateException("Player is not attached to world"); -+ } -+ -+ data.setTargetSendViewDistance(viewDistance); - } -+ // Paper end - implement view distances - - @Override - public T getClientOption(com.destroystokyo.paper.ClientOption type) { diff --git a/patches/server/Replace-ticket-level-propagator.patch b/patches/server/Replace-ticket-level-propagator.patch deleted file mode 100644 index 18144e1df4..0000000000 --- a/patches/server/Replace-ticket-level-propagator.patch +++ /dev/null @@ -1,258 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 21 Mar 2021 16:25:42 -0700 -Subject: [PATCH] Replace ticket level propagator - -Mojang's propagator is slow, and this isn't surprising -given it's built on the same utilities the vanilla light engine -is built on. The simple propagator I wrote is approximately 4x -faster when simulating player movement. For a long time timing -reports have shown this function take up significant tick, ( -approx 10% or more), and async sampling data shows the level -propagation alone takes up a significant amount. So this -should help with that. A big side effect is that mid-tick -will be more effective, since more time will be allocated -to actually processing chunk tasks vs the ticket level updates. - -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 @@ import net.minecraft.world.level.chunk.ChunkStatus; - import net.minecraft.world.level.chunk.LevelChunk; - import org.slf4j.Logger; - -+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Paper - public abstract class DistanceManager { - - static final Logger LOGGER = LogUtils.getLogger(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - private static final int BLOCK_TICKING_LEVEL_THRESHOLD = 33; - final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); - public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); -- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); -+ //private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Paper - replace ticket level propagator - public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used - private final TickingTracker tickingTicketsTracker = new TickingTracker(); - private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - this.chunkMap = chunkMap; // Paper - } - -+ // Paper start - replace ticket level propagator -+ protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() { -+ @Override -+ protected void rehash(int newN) { -+ // no downsizing allowed -+ if (newN < this.n) { -+ return; -+ } -+ super.rehash(newN); -+ } -+ }; -+ protected final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ticketLevelPropagator = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D( -+ (long coordinate, byte oldLevel, byte newLevel) -> { -+ DistanceManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel)); -+ } -+ ); -+ // function for converting between ticket levels and propagator levels and vice versa -+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects -+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator -+ // and the levels we get out of the propagator -+ -+ // this maps so that GOLDEN_TICKET + 1 will be 0 in the propagator, GOLDEN_TICKET will be 1, and so on -+ // we need GOLDEN_TICKET+1 as 0 because anything >= GOLDEN_TICKET+1 should be unloaded -+ public static int convertBetweenTicketLevels(final int level) { -+ return ChunkMap.MAX_CHUNK_DISTANCE - level + 1; -+ } -+ -+ protected final int getPropagatedTicketLevel(final long coordinate) { -+ return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate)); -+ } -+ -+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { -+ if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) { -+ this.ticketLevelPropagator.removeSource(coordinate); -+ } else { -+ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel)); -+ } -+ } -+ // Paper end - replace ticket level propagator -+ - protected void purgeStaleTickets() { - ++this.ticketTickCounter; - ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -+ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(entry.getValue())); // Paper - replace ticket level propagator - } - - if (((SortedArraySet) entry.getValue()).isEmpty()) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - @Nullable - protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); - -+ protected long ticketLevelUpdateCount; // Paper - replace ticket level propagator - public boolean runAllUpdates(ChunkMap chunkStorage) { - //this.f.a(); // Paper - no longer used - this.tickingTicketsTracker.runAllUpdates(); - org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper - this.playerTicketManager.runAllUpdates(); -- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); -- boolean flag = i != 0; -+ boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Paper - replace ticket level propagator - - if (flag) { - ; - } - -- // Paper start -- if (!this.pendingChunkUpdates.isEmpty()) { -- this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority -- while(!this.pendingChunkUpdates.isEmpty()) { -- ChunkHolder remove = this.pendingChunkUpdates.remove(); -- remove.isUpdateQueued = false; -- remove.updateFutures(chunkStorage, this.mainThreadExecutor); -- } -- } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority -- // Paper end -- return true; -- } else { -- if (!this.ticketsToRelease.isEmpty()) { -- LongIterator longiterator = this.ticketsToRelease.iterator(); -+ // Paper start - replace level propagator -+ ticket_update_loop: -+ while (!this.ticketLevelUpdates.isEmpty()) { -+ flag = true; - -- while (longiterator.hasNext()) { -- long j = longiterator.nextLong(); -+ boolean oldPolling = this.pollingPendingChunkUpdates; -+ this.pollingPendingChunkUpdates = true; -+ try { -+ for (java.util.Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { -+ Long2IntMap.Entry entry = iterator.next(); -+ long key = entry.getLongKey(); -+ int newLevel = entry.getIntValue(); -+ ChunkHolder chunk = this.getChunk(key); -+ -+ if (chunk == null && newLevel > ChunkMap.MAX_CHUNK_DISTANCE) { -+ // not loaded and it shouldn't be loaded! -+ continue; -+ } - -- if (this.getTickets(j).stream().anyMatch((ticket) -> { -- return ticket.getType() == TicketType.PLAYER; -- })) { -- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j); -+ int currentLevel = chunk == null ? ChunkMap.MAX_CHUNK_DISTANCE + 1 : chunk.getTicketLevel(); - -- if (playerchunk == null) { -- throw new IllegalStateException(); -+ if (currentLevel == newLevel) { -+ // nothing to do -+ continue; -+ } -+ -+ this.updateChunkScheduling(key, newLevel, chunk, currentLevel); -+ } -+ -+ long recursiveCheck = ++this.ticketLevelUpdateCount; -+ while (!this.ticketLevelUpdates.isEmpty()) { -+ long key = this.ticketLevelUpdates.firstLongKey(); -+ int newLevel = this.ticketLevelUpdates.removeFirstInt(); -+ ChunkHolder chunk = this.getChunk(key); -+ -+ if (chunk == null) { -+ if (newLevel <= ChunkMap.MAX_CHUNK_DISTANCE) { -+ throw new IllegalStateException("Expected chunk holder to be created"); - } -+ // not loaded and it shouldn't be loaded! -+ continue; -+ } - -- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); -+ int currentLevel = chunk.oldTicketLevel; - -- completablefuture.thenAccept((either) -> { -- this.mainThreadExecutor.execute(() -> { -- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { -- }, j, false)); -- }); -- }); -+ if (currentLevel == newLevel) { -+ // nothing to do -+ continue; -+ } -+ -+ chunk.updateFutures(chunkStorage, this.mainThreadExecutor); -+ if (recursiveCheck != this.ticketLevelUpdateCount) { -+ // back to the start, we must create player chunks and update the ticket level fields before -+ // processing the actual level updates -+ continue ticket_update_loop; - } - } - -- this.ticketsToRelease.clear(); -- } -+ for (;;) { -+ if (recursiveCheck != this.ticketLevelUpdateCount) { -+ continue ticket_update_loop; -+ } -+ ChunkHolder pendingUpdate = this.pendingChunkUpdates.poll(); -+ if (pendingUpdate == null) { -+ break; -+ } - -- return flag; -+ pendingUpdate.updateFutures(chunkStorage, this.mainThreadExecutor); -+ } -+ } finally { -+ this.pollingPendingChunkUpdates = oldPolling; -+ } - } -+ -+ return flag; -+ // Paper end - replace level propagator - } - boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority - -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - - ticket1.setCreatedTick(this.ticketTickCounter); - if (ticket.getTicketLevel() < j) { -- this.ticketTracker.update(i, ticket.getTicketLevel(), true); -+ this.updateTicketLevel(i, ticket.getTicketLevel()); // Paper - replace ticket level propagator - } - - return ticket == ticket1; // CraftBukkit -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - // Paper start - Chunk priority - int newLevel = getTicketLevelAt(arraysetsorted); - if (newLevel > oldLevel) { -- this.ticketTracker.update(i, newLevel, false); -+ this.updateTicketLevel(i, newLevel); // Paper // Paper - replace ticket level propagator - } - // Paper end - return removed; // CraftBukkit -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - } - - if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -+ this.updateTicketLevel(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue())); // Paper - replace ticket level propagator - } - - if (((SortedArraySet) entry.getValue()).isEmpty()) { -@@ -0,0 +0,0 @@ public abstract class DistanceManager { - SortedArraySet> tickets = entry.getValue(); - if (tickets.remove(target)) { - // copied from removeTicket -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); -+ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Paper - replace ticket level propagator - - // can't use entry after it's removed - if (tickets.isEmpty()) { diff --git a/patches/server/Retain-block-place-order-when-capturing-blockstates.patch b/patches/server/Retain-block-place-order-when-capturing-blockstates.patch index ba7a9b341f..f208686d96 100644 --- a/patches/server/Retain-block-place-order-when-capturing-blockstates.patch +++ b/patches/server/Retain-block-place-order-when-capturing-blockstates.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Fri, 7 Aug 2020 04:27:56 -0700 Subject: [PATCH] Retain block place order when capturing blockstates diff --git a/patches/server/Rewrite-chunk-system.patch b/patches/server/Rewrite-chunk-system.patch new file mode 100644 index 0000000000..4a31226d99 --- /dev/null +++ b/patches/server/Rewrite-chunk-system.patch @@ -0,0 +1,18056 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 11 Mar 2021 02:32:30 -0800 +Subject: [PATCH] Rewrite chunk system + + +diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +@@ -0,0 +0,0 @@ public final class StarLightInterface { + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + +- protected final LightQueue lightQueue = new LightQueue(this); ++ public final io.papermc.paper.chunk.system.light.LightQueue lightQueue; // Paper - replace light queue + + protected final LayerLightEventListener skyReader; + protected final LayerLightEventListener blockReader; + protected final boolean isClientSide; + +- protected final int minSection; +- protected final int maxSection; ++ public final int minSection; // Paper - public ++ public final int maxSection; // Paper - public + protected final int minLightSection; + protected final int maxLightSection; + +@@ -0,0 +0,0 @@ public final class StarLightInterface { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; ++ this.lightQueue = new io.papermc.paper.chunk.system.light.LightQueue(this); // Paper - replace light queue + } + + protected int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { +@@ -0,0 +0,0 @@ public final class StarLightInterface { + return this.lightAccess; + } + +- protected final SkyStarLightEngine getSkyLightEngine() { ++ public final SkyStarLightEngine getSkyLightEngine() { // Paper - public + if (this.cachedSkyPropagators == null) { + return null; + } +@@ -0,0 +0,0 @@ public final class StarLightInterface { + return ret; + } + +- protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { ++ public final void releaseSkyLightEngine(final SkyStarLightEngine engine) { // Paper - public + if (this.cachedSkyPropagators == null) { + return; + } +@@ -0,0 +0,0 @@ public final class StarLightInterface { + } + } + +- protected final BlockStarLightEngine getBlockLightEngine() { ++ public final BlockStarLightEngine getBlockLightEngine() { // Paper - public + if (this.cachedBlockPropagators == null) { + return null; + } +@@ -0,0 +0,0 @@ public final class StarLightInterface { + return ret; + } + +- protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { ++ public final void releaseBlockLightEngine(final BlockStarLightEngine engine) { // Paper - public + if (this.cachedBlockPropagators == null) { + return; + } +@@ -0,0 +0,0 @@ public final class StarLightInterface { + } + + public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { +- this.lightQueue.queueChunkLighting(pos, run); ++ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue + } + + public void removeChunkTasks(final ChunkPos pos) { +- this.lightQueue.removeChunk(pos); ++ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue + } + + public void propagateChanges() { +- if (this.lightQueue.isEmpty()) { +- return; +- } +- +- final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); +- final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); +- +- try { +- LightQueue.ChunkTasks task; +- while ((task = this.lightQueue.removeFirstTask()) != null) { +- if (task.lightTasks != null) { +- for (final Runnable run : task.lightTasks) { +- run.run(); +- } +- } +- +- final long coordinate = task.chunkCoordinate; +- final int chunkX = CoordinateUtils.getChunkX(coordinate); +- final int chunkZ = CoordinateUtils.getChunkZ(coordinate); +- +- final Set positions = task.changedPositions; +- final Boolean[] sectionChanges = task.changedSectionSet; +- +- if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { +- skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); +- } +- if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { +- blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); +- } +- +- if (skyEngine != null && task.queuedEdgeChecksSky != null) { +- skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); +- } +- if (blockEngine != null && task.queuedEdgeChecksBlock != null) { +- blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); +- } +- +- task.onComplete.complete(null); +- } +- } finally { +- this.releaseSkyLightEngine(skyEngine); +- this.releaseBlockLightEngine(blockEngine); +- } ++ throw new UnsupportedOperationException("No longer implemented, task draining is now performed by the light thread"); // Paper - replace light queue + } + + protected static final class LightQueue { +diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/co/aikar/timings/TimingsExport.java ++++ b/src/main/java/co/aikar/timings/TimingsExport.java +@@ -0,0 +0,0 @@ public class TimingsExport extends Thread { + pair("gamerules", toObjectMapper(world.getWorld().getGameRules(), rule -> { + return pair(rule, world.getWorld().getGameRuleValue(rule)); + })), +- pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()) ++ // Paper start - replace chunk loader system ++ pair("ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()), ++ pair("no-ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()), ++ pair("sending-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance()) ++ // Paper end - replace chunk loader system + )); + })); + +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; ++ ++@Deprecated(forRemoval = true) ++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) ++ * @deprecated ++ */ ++@Deprecated(forRemoval = true) ++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 { ++ // Paper - fail hard on usage ++ } ++ } ++ ++ 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) { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ public CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final boolean poiData) { ++ // Paper start - rewrite chunk system ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.getPendingWrite( ++ world, chunkX, chunkZ, poiData ? io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA : ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA ++ ); ++ // Paper end - rewrite chunk system ++ } ++ ++ /** ++ * 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) { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ /** ++ * 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 { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ private void scheduleWrite(final ChunkDataController dataController, final ServerLevel world, ++ final int chunkX, final int chunkZ, final CompoundTag data, final int priority, final long writeCounter) { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ /** ++ * 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(); ++ // Paper start - rewrite chunk system ++ final java.util.List types = new java.util.ArrayList<>(); ++ if (readPoiData) { ++ types.add(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA); ++ } ++ if (readChunkData) { ++ types.add(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority newPriority; ++ switch (priority) { ++ case PrioritizedTaskQueue.HIGHEST_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING; ++ case PrioritizedTaskQueue.HIGHER_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST; ++ case PrioritizedTaskQueue.HIGH_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH; ++ case PrioritizedTaskQueue.NORMAL_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL; ++ case PrioritizedTaskQueue.LOW_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.LOW; ++ case PrioritizedTaskQueue.LOWEST_PRIORITY -> newPriority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.IDLE; ++ default -> throw new IllegalStateException("Legacy priority " + priority + " should be valid"); ++ } ++ final Consumer transformComplete = (io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileData data) -> { ++ if (readPoiData) { ++ if (data.getThrowable(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA) != null) { ++ complete.poiData = FAILURE_VALUE; ++ } else { ++ complete.poiData = data.getData(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA); ++ } ++ } ++ ++ if (readChunkData) { ++ if (data.getThrowable(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA) != null) { ++ complete.chunkData = FAILURE_VALUE; ++ } else { ++ complete.chunkData = data.getData(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ } ++ ++ onComplete.accept(complete); ++ }; ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, transformComplete, intendingToBlock, newPriority, types.toArray(new io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType[0])); ++ // Paper end - rewrite chunk system ++ ++ } ++ ++ // 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) { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ /** ++ * 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) { ++ throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ } ++ ++ 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 == null) + ", hash: " + this.hashCode(); // Paper - TODO rewrite chunk system ++ } ++ ++ /* ++ * ++ * 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() { ++ if (true) throw new IllegalStateException("Shouldn't get here, use RegionFileIOThread"); // Paper - rewrite chunk system, fail hard on usage ++ 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; ++ ++@Deprecated(forRemoval = true) ++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; ++ ++@Deprecated(forRemoval = true) ++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/io/papermc/paper/chunk/PlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk; ++ ++import com.destroystokyo.paper.util.misc.PlayerAreaMap; ++import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.IntervalledCounter; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; ++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; ++import net.minecraft.server.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.*; ++import net.minecraft.util.Mth; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.LevelChunk; ++import org.apache.commons.lang3.mutable.MutableObject; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.TreeSet; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public final class PlayerChunkLoader { ++ ++ public static final int MIN_VIEW_DISTANCE = 2; ++ public static final int MAX_VIEW_DISTANCE = 32; ++ ++ public static final int TICK_TICKET_LEVEL = 31; ++ public static final int LOADED_TICKET_LEVEL = 33; ++ ++ public static int getTickViewDistance(final Player player) { ++ return getTickViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getTickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); ++ if (data == null) { ++ return level.chunkSource.chunkMap.playerChunkManager.getTargetTickViewDistance(); ++ } ++ return data.getTargetTickViewDistance(); ++ } ++ ++ public static int getLoadViewDistance(final Player player) { ++ return getLoadViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); ++ if (data == null) { ++ return level.chunkSource.chunkMap.playerChunkManager.getLoadDistance(); ++ } ++ return data.getLoadDistance(); ++ } ++ ++ public static int getSendViewDistance(final Player player) { ++ return getSendViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getSendViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level; ++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player); ++ if (data == null) { ++ return level.chunkSource.chunkMap.playerChunkManager.getTargetSendDistance(); ++ } ++ return data.getTargetSendViewDistance(); ++ } ++ ++ protected final ChunkMap chunkMap; ++ protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f); ++ protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f); ++ ++ protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { ++ if (p1 == p2) { ++ return 0; ++ } ++ ++ final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst(); ++ final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst(); ++ ++ final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority); ++ ++ final int lastLoadTimeCompare = Long.compare(p1.lastChunkLoad, p2.lastChunkLoad); ++ ++ if ((holder1 == null || holder2 == null || lastLoadTimeCompare == 0 || holder1.priority < 0.0 || holder2.priority < 0.0) && priorityCompare != 0) { ++ return priorityCompare; ++ } ++ ++ if (lastLoadTimeCompare != 0) { ++ return lastLoadTimeCompare; ++ } ++ ++ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); ++ ++ if (idCompare != 0) { ++ return idCompare; ++ } ++ ++ // last resort ++ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); ++ }); ++ ++ protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> { ++ if (p1 == p2) { ++ return 0; ++ } ++ ++ final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget); ++ if (timeCompare != 0) { ++ return timeCompare; ++ } ++ ++ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId()); ++ ++ if (idCompare != 0) { ++ return idCompare; ++ } ++ ++ // last resort ++ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); ++ }); ++ ++ ++ // no throttling is applied below this VD for loading ++ ++ /** ++ * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded. ++ */ ++ public final PlayerAreaMap broadcastMap; ++ ++ /** ++ * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded. ++ */ ++ public final PlayerAreaMap loadMap; ++ ++ /** ++ * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus, ++ * this map is always representing the chunks we are actually going to load. ++ */ ++ public final PlayerAreaMap loadTicketCleanup; ++ ++ /** ++ * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen. ++ */ ++ public final PlayerAreaMap tickMap; ++ ++ /** ++ * -1 if defaulting to [load distance], else always in [2, load distance] ++ */ ++ protected int rawSendDistance = -1; ++ ++ /** ++ * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1] ++ */ ++ protected int rawLoadDistance = -1; ++ ++ /** ++ * Never -1, always in [2, 32] ++ */ ++ protected int rawTickDistance = -1; ++ ++ // methods to bridge for API ++ ++ public int getTargetTickViewDistance() { ++ return this.getTickDistance(); ++ } ++ ++ public void setTargetTickViewDistance(final int distance) { ++ this.setTickDistance(distance); ++ } ++ ++ public int getTargetNoTickViewDistance() { ++ return this.getLoadDistance() - 1; ++ } ++ ++ public void setTargetNoTickViewDistance(final int distance) { ++ this.setLoadDistance(distance == -1 ? -1 : distance + 1); ++ } ++ ++ public int getTargetSendDistance() { ++ return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance; ++ } ++ ++ public void setTargetSendDistance(final int distance) { ++ this.setSendDistance(distance); ++ } ++ ++ // internal methods ++ ++ public int getSendDistance() { ++ final int loadDistance = this.getLoadDistance(); ++ return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance); ++ } ++ ++ public void setSendDistance(final int distance) { ++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + ", or -1, got: " + distance); ++ } ++ this.rawSendDistance = distance; ++ } ++ ++ public int getLoadDistance() { ++ final int tickDistance = this.getTickDistance(); ++ return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance); ++ } ++ ++ public void setLoadDistance(final int distance) { ++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Load distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + ", or -1, got: " + distance); ++ } ++ this.rawLoadDistance = distance; ++ } ++ ++ public int getTickDistance() { ++ return this.rawTickDistance; ++ } ++ ++ public void setTickDistance(final int distance) { ++ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) { ++ throw new IllegalArgumentException("View distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + ", got: " + distance); ++ } ++ this.rawTickDistance = distance; ++ } ++ ++ /* ++ Players have 3 different types of view distance: ++ 1. Sending view distance ++ 2. Loading view distance ++ 3. Ticking view distance ++ ++ But for configuration purposes (and API) there are: ++ 1. No-tick view distance ++ 2. Tick view distance ++ 3. Broadcast view distance ++ ++ These aren't always the same as the types we represent internally. ++ ++ Loading view distance is always max(no-tick + 1, tick + 1) ++ - no-tick has 1 added because clients need an extra radius to render chunks ++ - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking ++ ++ Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means ++ it loads chunks in radius load-view-distance + 1. ++ ++ The maximum value for send view distance is the load view distance. API can set it lower. ++ */ ++ ++ public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) { ++ this.chunkMap = chunkMap; ++ this.broadcastMap = new PlayerAreaMap(pooledHashSets, ++ null, ++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ); ++ }); ++ this.loadMap = new PlayerAreaMap(pooledHashSets, ++ null, ++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (newState != null) { ++ return; ++ } ++ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ)); ++ }); ++ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets, ++ null, ++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (newState != null) { ++ return; ++ } ++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); ++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); ++ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) { ++ --PlayerChunkLoader.this.concurrentChunkLoads; ++ } ++ }); ++ this.tickMap = new PlayerAreaMap(pooledHashSets, ++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (newState.size() != 1) { ++ return; ++ } ++ LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); ++ if (chunk == null || !chunk.areNeighboursLoaded(2)) { ++ return; ++ } ++ ++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); ++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); ++ }, ++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (newState != null) { ++ return; ++ } ++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); ++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); ++ }); ++ } ++ ++ protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet(); ++ protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet(); ++ ++ public boolean isChunkNearPlayers(final int chunkX, final int chunkZ) { ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); ++ ++ return playersInSendRange != null; ++ } ++ ++ public void onChunkPostProcessing(final int chunkX, final int chunkZ) { ++ this.onChunkSendReady(chunkX, chunkZ); ++ } ++ ++ private boolean chunkNeedsPostProcessing(final int chunkX, final int chunkZ) { ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); ++ ++ if (chunk == null) { ++ return false; ++ } ++ ++ final LevelChunk levelChunk = chunk.getSendingChunk(); ++ ++ return levelChunk != null && !levelChunk.isPostProcessingDone; ++ } ++ ++ // rets whether the chunk is at a loaded stage that is ready to be sent to players ++ public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) { ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key); ++ ++ if (chunk == null) { ++ return false; ++ } ++ ++ final LevelChunk levelChunk = chunk.getSendingChunk(); ++ ++ return levelChunk != null && levelChunk.isPostProcessingDone && this.isTargetedForPlayerLoad.contains(key); ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { ++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerLoaderData data = this.playerMap.get(player); ++ if (data == null) { ++ return false; ++ } ++ ++ return data.hasSentChunk(chunkX, chunkZ); ++ } ++ ++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerLoaderData data = this.playerMap.get(player); ++ if (data == null) { ++ return false; ++ } ++ ++ final boolean center = data.hasSentChunk(chunkX, chunkZ); ++ if (!center) { ++ return false; ++ } ++ ++ return !(data.hasSentChunk(chunkX - 1, chunkZ) && data.hasSentChunk(chunkX + 1, chunkZ) && ++ data.hasSentChunk(chunkX, chunkZ - 1) && data.hasSentChunk(chunkX, chunkZ + 1)); ++ } ++ ++ protected int getMaxConcurrentChunkSends() { ++ return GlobalConfiguration.get().chunkLoading.maxConcurrentSends; ++ } ++ ++ protected int getMaxChunkLoads() { ++ double config = GlobalConfiguration.get().chunkLoading.playerMaxConcurrentLoads; ++ double max = GlobalConfiguration.get().chunkLoading.globalMaxConcurrentLoads; ++ return (int)Math.ceil(Math.min(config * MinecraftServer.getServer().getPlayerCount(), max <= 1.0 ? Double.MAX_VALUE : max)); ++ } ++ ++ protected long getTargetSendPerPlayerAddend() { ++ return GlobalConfiguration.get().chunkLoading.targetPlayerChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / GlobalConfiguration.get().chunkLoading.targetPlayerChunkSendRate); ++ } ++ ++ protected long getMaxSendAddend() { ++ return GlobalConfiguration.get().chunkLoading.globalMaxChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / GlobalConfiguration.get().chunkLoading.globalMaxChunkSendRate); ++ } ++ ++ public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) { ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos); ++ } ++ ++ public void onChunkSendReady(final int chunkX, final int chunkZ) { ++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ); ++ ++ if (playersInSendRange == null) { ++ return; ++ } ++ ++ final Object[] rawData = playersInSendRange.getBackingSet(); ++ for (int i = 0, len = rawData.length; i < len; ++i) { ++ final Object raw = rawData[i]; ++ ++ if (!(raw instanceof ServerPlayer)) { ++ continue; ++ } ++ this.onChunkSendReady((ServerPlayer)raw, chunkX, chunkZ); ++ } ++ } ++ ++ public void onChunkSendReady(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerLoaderData data = this.playerMap.get(player); ++ ++ if (data == null) { ++ return; ++ } ++ ++ if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) { ++ // if we don't have player tickets, then the load logic will pick this up and queue to send ++ return; ++ } ++ ++ if (!data.chunksToBeSent.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ // don't queue to send, we don't want the chunk ++ return; ++ } ++ ++ final long playerPos = this.broadcastMap.getLastCoordinate(player); ++ final int playerChunkX = CoordinateUtils.getChunkX(playerPos); ++ final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos); ++ final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ); ++ ++ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0); ++ data.sendQueue.add(holder); ++ } ++ ++ public void onChunkLoad(final int chunkX, final int chunkZ) { ++ if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ --this.concurrentChunkLoads; ++ } ++ } ++ ++ public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerLoaderData data = this.playerMap.get(player); ++ ++ if (data == null) { ++ return; ++ } ++ ++ data.unloadChunk(chunkX, chunkZ); ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread("Cannot add player async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ final PlayerLoaderData data = new PlayerLoaderData(player, this); ++ if (this.playerMap.putIfAbsent(player, data) == null) { ++ data.update(); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread("Cannot remove player async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ ++ final PlayerLoaderData loaderData = this.playerMap.remove(player); ++ if (loaderData == null) { ++ return; ++ } ++ loaderData.remove(); ++ this.chunkLoadQueue.remove(loaderData); ++ this.chunkSendQueue.remove(loaderData); ++ this.chunkSendWaitQueue.remove(loaderData); ++ synchronized (this.sendingChunkCounts) { ++ final int count = this.sendingChunkCounts.removeInt(loaderData); ++ if (count != 0) { ++ concurrentChunkSends.getAndAdd(-count); ++ } ++ } ++ } ++ ++ public void updatePlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread("Cannot update player async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ final PlayerLoaderData loaderData = this.playerMap.get(player); ++ if (loaderData != null) { ++ loaderData.update(); ++ } ++ } ++ ++ public PlayerLoaderData getData(final ServerPlayer player) { ++ return this.playerMap.get(player); ++ } ++ ++ public void tick() { ++ TickThread.ensureTickThread("Cannot tick async"); ++ for (final PlayerLoaderData data : this.playerMap.values()) { ++ data.update(); ++ } ++ this.tickMidTick(); ++ } ++ ++ protected static final AtomicInteger concurrentChunkSends = new AtomicInteger(); ++ protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>(); ++ private static long nextChunkSend; ++ private void trySendChunks() { ++ final long time = System.nanoTime(); ++ if (time < nextChunkSend) { ++ return; ++ } ++ // drain entries from wait queue ++ while (!this.chunkSendWaitQueue.isEmpty()) { ++ final PlayerLoaderData data = this.chunkSendWaitQueue.first(); ++ ++ if (data.nextChunkSendTarget > time) { ++ break; ++ } ++ ++ this.chunkSendWaitQueue.pollFirst(); ++ ++ this.chunkSendQueue.add(data); ++ } ++ ++ if (this.chunkSendQueue.isEmpty()) { ++ return; ++ } ++ ++ final int maxSends = this.getMaxConcurrentChunkSends(); ++ final long nextPlayerDeadline = this.getTargetSendPerPlayerAddend() + time; ++ for (;;) { ++ if (this.chunkSendQueue.isEmpty()) { ++ break; ++ } ++ final int currSends = concurrentChunkSends.get(); ++ if (currSends >= maxSends) { ++ break; ++ } ++ ++ if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) { ++ continue; ++ } ++ ++ // send chunk ++ ++ final PlayerLoaderData data = this.chunkSendQueue.removeFirst(); ++ ++ final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst(); ++ if (queuedSend == null) { ++ concurrentChunkSends.getAndDecrement(); // we never sent, so decrease ++ // stop iterating over players who have nothing to send ++ if (this.chunkSendQueue.isEmpty()) { ++ // nothing left ++ break; ++ } ++ continue; ++ } ++ ++ if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) { ++ throw new IllegalStateException(); ++ } ++ ++ data.nextChunkSendTarget = nextPlayerDeadline; ++ this.chunkSendWaitQueue.add(data); ++ ++ synchronized (this.sendingChunkCounts) { ++ this.sendingChunkCounts.addTo(data, 1); ++ } ++ ++ data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> { ++ synchronized (this.sendingChunkCounts) { ++ final int count = this.sendingChunkCounts.getInt(data); ++ if (count == 0) { ++ // disconnected, so we don't need to decrement: it will be decremented for us ++ return; ++ } ++ if (count == 1) { ++ this.sendingChunkCounts.removeInt(data); ++ } else { ++ this.sendingChunkCounts.put(data, count - 1); ++ } ++ } ++ ++ concurrentChunkSends.getAndDecrement(); ++ }); ++ ++ nextChunkSend = this.getMaxSendAddend() + time; ++ if (time < nextChunkSend) { ++ break; ++ } ++ } ++ } ++ ++ protected int concurrentChunkLoads; ++ // this interval prevents bursting a lot of chunk loads ++ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_SHORT = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms ++ // this interval ensures the rate is kept between ticks correctly ++ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_LONG = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms ++ private void tryLoadChunks() { ++ if (this.chunkLoadQueue.isEmpty()) { ++ return; ++ } ++ ++ final int maxLoads = this.getMaxChunkLoads(); ++ final long time = System.nanoTime(); ++ boolean updatedCounters = false; ++ for (;;) { ++ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst(); ++ ++ data.lastChunkLoad = time; ++ ++ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst(); ++ if (queuedLoad == null) { ++ if (this.chunkLoadQueue.isEmpty()) { ++ break; ++ } ++ continue; ++ } ++ ++ if (!updatedCounters) { ++ updatedCounters = true; ++ TICKET_ADDITION_COUNTER_SHORT.updateCurrentTime(time); ++ TICKET_ADDITION_COUNTER_LONG.updateCurrentTime(time); ++ data.ticketAdditionCounterShort.updateCurrentTime(time); ++ data.ticketAdditionCounterLong.updateCurrentTime(time); ++ } ++ ++ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { ++ // already loaded! ++ data.loadQueue.pollFirst(); // already loaded so we just skip ++ this.chunkLoadQueue.add(data); ++ ++ // ensure the chunk is queued to send ++ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); ++ continue; ++ } ++ ++ final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); ++ ++ final double priority = queuedLoad.priority; ++ // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present. ++ // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the ++ // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they ++ // aren't required, it bypasses the limiter system. ++ boolean unloadedTargetChunk = false; ++ unloaded_check: ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ final int offX = queuedLoad.chunkX + dx; ++ final int offZ = queuedLoad.chunkZ + dz; ++ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) { ++ unloadedTargetChunk = true; ++ break unloaded_check; ++ } ++ } ++ } ++ if (unloadedTargetChunk && priority >= 0.0) { ++ // priority >= 0.0 implies rate limited chunks ++ ++ final int currentChunkLoads = this.concurrentChunkLoads; ++ if (currentChunkLoads >= maxLoads || (GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= GlobalConfiguration.get().chunkLoading.globalMaxChunkLoadRate)) ++ || (GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate > 0.0 && (data.ticketAdditionCounterShort.getRate() >= GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate || data.ticketAdditionCounterLong.getRate() >= GlobalConfiguration.get().chunkLoading.playerMaxChunkLoadRate))) { ++ // don't poll, we didn't load it ++ this.chunkLoadQueue.add(data); ++ break; ++ } ++ } ++ ++ // can only poll after we decide to load ++ data.loadQueue.pollFirst(); ++ ++ // now that we've polled we can re-add to load queue ++ this.chunkLoadQueue.add(data); ++ ++ // add necessary tickets to load chunk up to send-ready ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ final int offX = queuedLoad.chunkX + dx; ++ final int offZ = queuedLoad.chunkZ + dz; ++ final ChunkPos chunkPos = new ChunkPos(offX, offZ); ++ ++ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos); ++ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) { ++ continue; ++ } ++ ++ if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) { ++ // won't reach here if unloadedTargetChunk is false ++ ++this.concurrentChunkLoads; ++ TICKET_ADDITION_COUNTER_SHORT.addTime(time); ++ TICKET_ADDITION_COUNTER_LONG.addTime(time); ++ data.ticketAdditionCounterShort.addTime(time); ++ data.ticketAdditionCounterLong.addTime(time); ++ } ++ } ++ } ++ ++ // mark that we've added tickets here ++ this.isTargetedForPlayerLoad.add(chunkKey); ++ ++ // it's possible all we needed was the player tickets to queue up the send. ++ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) { ++ // yup, all we needed. ++ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ); ++ } else if (this.chunkNeedsPostProcessing(queuedLoad.chunkX, queuedLoad.chunkZ)) { ++ // requires post processing ++ this.chunkMap.mainThreadExecutor.execute(() -> { ++ final long key = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ); ++ final ChunkHolder holder = PlayerChunkLoader.this.chunkMap.getVisibleChunkIfPresent(key); ++ ++ if (holder == null) { ++ return; ++ } ++ ++ final LevelChunk chunk = holder.getSendingChunk(); ++ ++ if (chunk != null && !chunk.isPostProcessingDone) { ++ chunk.postProcessGeneration(); ++ } ++ }); ++ } ++ } ++ } ++ ++ public void tickMidTick() { ++ // try to send more chunks ++ this.trySendChunks(); ++ ++ // try to queue more chunks to load ++ this.tryLoadChunks(); ++ } ++ ++ static final class ChunkPriorityHolder { ++ public final int chunkX; ++ public final int chunkZ; ++ public final int manhattanDistanceToPlayer; ++ public final double priority; ++ ++ public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) { ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.manhattanDistanceToPlayer = manhattanDistanceToPlayer; ++ this.priority = priority; ++ } ++ } ++ ++ public static final class PlayerLoaderData { ++ ++ protected static final float FOV = 110.0f; ++ protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0; ++ ++ // Player max sprint speed is approximately 8m/s ++ protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0); ++ protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f; ++ ++ protected double lastLocX = Double.NEGATIVE_INFINITY; ++ protected double lastLocZ = Double.NEGATIVE_INFINITY; ++ ++ protected int lastChunkX = Integer.MIN_VALUE; ++ protected int lastChunkZ = Integer.MIN_VALUE; ++ ++ // this is corrected so that 0 is along the positive x-axis ++ protected float lastYaw = Float.NEGATIVE_INFINITY; ++ ++ protected int lastSendDistance = Integer.MIN_VALUE; ++ protected int lastLoadDistance = Integer.MIN_VALUE; ++ protected int lastTickDistance = Integer.MIN_VALUE; ++ protected boolean usingLookingPriority; ++ ++ protected final ServerPlayer player; ++ protected final PlayerChunkLoader loader; ++ ++ // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field ++ // in a comparator! ++ protected final ArrayDeque loadQueue = new ArrayDeque<>(); ++ protected final LongOpenHashSet sentChunks = new LongOpenHashSet(); ++ protected final LongOpenHashSet chunksToBeSent = new LongOpenHashSet(); ++ ++ protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { ++ final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer); ++ if (distanceCompare != 0) { ++ return distanceCompare; ++ } ++ ++ final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX); ++ if (coordinateXCompare != 0) { ++ return coordinateXCompare; ++ } ++ ++ return Integer.compare(p1.chunkZ, p2.chunkZ); ++ }); ++ ++ protected int sendViewDistance = -1; ++ protected int loadViewDistance = -1; ++ protected int tickViewDistance = -1; ++ ++ protected long nextChunkSendTarget; ++ ++ // this interval prevents bursting a lot of chunk loads ++ protected final IntervalledCounter ticketAdditionCounterShort = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms ++ // this ensures the rate is kept between ticks correctly ++ protected final IntervalledCounter ticketAdditionCounterLong = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms ++ ++ public long lastChunkLoad; ++ ++ public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) { ++ this.player = player; ++ this.loader = loader; ++ } ++ ++ // these view distance methods are for api ++ public int getTargetSendViewDistance() { ++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; ++ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); ++ final int clientViewDistance = this.getClientViewDistance(); ++ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!GlobalConfiguration.get().chunkLoading.autoconfigSendDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); ++ return sendViewDistance; ++ } ++ ++ public void setTargetSendViewDistance(final int distance) { ++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send view distance must be a number between " + MIN_VIEW_DISTANCE + " and " + (MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.sendViewDistance = distance; ++ } ++ ++ public int getTargetNoTickViewDistance() { ++ return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1; ++ } ++ ++ public void setTargetNoTickViewDistance(final int distance) { ++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("Simulation distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + " or -1, got: " + distance); ++ } ++ this.loadViewDistance = distance == -1 ? -1 : distance + 1; ++ } ++ ++ public int getTargetTickViewDistance() { ++ return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; ++ } ++ ++ public void setTargetTickViewDistance(final int distance) { ++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("View distance must be a number between " + MIN_VIEW_DISTANCE + " and " + MAX_VIEW_DISTANCE + " or -1, got: " + distance); ++ } ++ this.tickViewDistance = distance; ++ } ++ ++ protected int getLoadDistance() { ++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; ++ ++ return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); ++ } ++ ++ public boolean hasSentChunk(final int chunkX, final int chunkZ) { ++ return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) { ++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, ++ new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded ++ this.player.connection.connection.execute(onChunkSend); ++ } else { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ public void unloadChunk(final int chunkX, final int chunkZ) { ++ if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player, ++ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded ++ } ++ } ++ ++ protected static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, ++ final int sendRadius) { ++ // expect sendRadius to be = 1 + target viewable radius ++ return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius); ++ } ++ ++ protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point ++ final double p2x, final double p2z, // triangle point ++ final double p3x, final double p3z, // triangle point ++ ++ final double targetX, final double targetZ) { // point ++ // from barycentric coordinates: ++ // targetX = a*p1x + b*p2x + c*p3x ++ // targetZ = a*p1z + b*p2z + c*p3z ++ // 1.0 = a*1.0 + b*1.0 + c*1.0 ++ // where a, b, c >= 0.0 ++ // so, if any of a, b, c are less-than zero then there is no intersection. ++ ++ // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z)) ++ // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d ++ // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d ++ // c = 1.0 - a - b ++ ++ final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z); ++ final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d; ++ ++ if (a < 0.0 || a > 1.0) { ++ return false; ++ } ++ ++ final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d; ++ if (b < 0.0 || b > 1.0) { ++ return false; ++ } ++ ++ final double c = 1.0 - a - b; ++ ++ return c >= 0.0 && c <= 1.0; ++ } ++ ++ public void remove() { ++ this.loader.broadcastMap.remove(this.player); ++ this.loader.loadMap.remove(this.player); ++ this.loader.loadTicketCleanup.remove(this.player); ++ this.loader.tickMap.remove(this.player); ++ } ++ ++ protected int getClientViewDistance() { ++ return this.player.clientViewDistance == null ? -1 : Math.max(0, this.player.clientViewDistance.intValue()); ++ } ++ ++ public void update() { ++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance; ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = this.getClientViewDistance(); ++ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!GlobalConfiguration.get().chunkLoading.autoconfigSendDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance); ++ ++ final double posX = this.player.getX(); ++ final double posZ = this.player.getZ(); ++ final float yaw = MCUtil.normalizeYaw(this.player.getYRot() + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis ++ ++ // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them. ++ final boolean useLookPriority = GlobalConfiguration.get().chunkLoading.enableFrustumPriority && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD || ++ this.player.getAbilities().flying); ++ ++ // make sure we're in the send queue ++ this.loader.chunkSendWaitQueue.add(this); ++ ++ if ( ++ // has view distance stayed the same? ++ sendViewDistance == this.lastSendDistance ++ && loadViewDistance == this.lastLoadDistance ++ && tickViewDistance == this.lastTickDistance ++ ++ && (this.usingLookingPriority ? ( ++ // has our block stayed the same (this also accounts for chunk change)? ++ Mth.floor(this.lastLocX) == Mth.floor(posX) ++ && Mth.floor(this.lastLocZ) == Mth.floor(posZ) ++ ) : ( ++ // has our chunk stayed the same ++ (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4) ++ && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4) ++ )) ++ ++ // has our decision about look priority changed? ++ && this.usingLookingPriority == useLookPriority ++ ++ // if we are currently using look priority, has our yaw stayed within recalc threshold? ++ && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD) ++ ) { ++ // nothing we care about changed, so we're not re-calculating ++ return; ++ } ++ ++ final int centerChunkX = Mth.floor(posX) >> 4; ++ final int centerChunkZ = Mth.floor(posZ) >> 4; ++ ++ final boolean needsChunkCenterUpdate = (centerChunkX != this.lastChunkX) || (centerChunkZ != this.lastChunkZ); ++ this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance); ++ this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance); ++ this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1); ++ this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance); ++ ++ if (sendViewDistance != this.lastSendDistance) { ++ // update the view radius for client ++ // note that this should be after the map calls because the client wont expect unload calls not in its VD ++ // and it's possible we decreased VD here ++ this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance)); ++ } ++ if (tickViewDistance != this.lastTickDistance) { ++ this.player.connection.send(new ClientboundSetSimulationDistancePacket(tickViewDistance)); ++ } ++ ++ this.lastLocX = posX; ++ this.lastLocZ = posZ; ++ this.lastYaw = yaw; ++ this.lastSendDistance = sendViewDistance; ++ this.lastLoadDistance = loadViewDistance; ++ this.lastTickDistance = tickViewDistance; ++ this.usingLookingPriority = useLookPriority; ++ ++ this.lastChunkX = centerChunkX; ++ this.lastChunkZ = centerChunkZ; ++ ++ // points for player "view" triangle: ++ ++ // obviously, the player pos is a vertex ++ final double p1x = posX; ++ final double p1z = posZ; ++ ++ // to the left of the looking direction ++ final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector ++ + p1x; // offset vector ++ final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector ++ + p1z; // offset vector ++ ++ // to the right of the looking direction ++ final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector ++ + p1x; // offset vector ++ final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector ++ + p1z; // offset vector ++ ++ // now that we have all of our points, we can recalculate the load queue ++ ++ final List loadQueue = new ArrayList<>(); ++ ++ // clear send queue, we are re-sorting ++ this.sendQueue.clear(); ++ // clear chunk want set, vd/position might have changed ++ this.chunksToBeSent.clear(); ++ ++ final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance); ++ ++ for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) { ++ for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) { ++ final int chunkX = dx + centerChunkX; ++ final int chunkZ = dz + centerChunkZ; ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final boolean sendChunk = squareDistance <= sendViewDistance && wantChunkLoaded(centerChunkX, centerChunkZ, chunkX, chunkZ, sendViewDistance); ++ ++ if (this.hasSentChunk(chunkX, chunkZ)) { ++ // already sent (which means it is also loaded) ++ if (!sendChunk) { ++ // have sent the chunk, but don't want it anymore ++ // unload it now ++ this.unloadChunk(chunkX, chunkZ); ++ } ++ continue; ++ } ++ ++ final boolean loadChunk = squareDistance <= loadViewDistance; ++ ++ final boolean prioritised = useLookPriority && triangleIntersects( ++ // prioritisation triangle ++ p1x, p1z, p2x, p2z, p3x, p3z, ++ ++ // center of chunk ++ (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8) ++ ); ++ ++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); ++ ++ final double priority; ++ ++ if (squareDistance <= GlobalConfiguration.get().chunkLoading.minLoadRadius) { ++ // priority should be negative, and we also want to order it from center outwards ++ // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest ++ priority = -((2 * GlobalConfiguration.get().chunkLoading.minLoadRadius + 1) - manhattanDistance); ++ } else { ++ if (prioritised) { ++ // we don't prioritise these chunks above others because we also want to make sure some chunks ++ // will be loaded if the player changes direction ++ priority = (double)manhattanDistance / 6.0; ++ } else { ++ priority = (double)manhattanDistance; ++ } ++ } ++ ++ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority); ++ ++ if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) { ++ if (loadChunk) { ++ loadQueue.add(holder); ++ if (sendChunk) { ++ this.chunksToBeSent.add(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ } ++ } else { ++ // loaded but not sent: so queue it! ++ if (sendChunk) { ++ this.sendQueue.add(holder); ++ } ++ } ++ } ++ } ++ ++ loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> { ++ return Double.compare(p1.priority, p2.priority); ++ }); ++ ++ // we're modifying loadQueue, must remove ++ this.loader.chunkLoadQueue.remove(this); ++ ++ this.loadQueue.clear(); ++ this.loadQueue.addAll(loadQueue); ++ ++ // must re-add ++ this.loader.chunkLoadQueue.add(this); ++ ++ // update the chunk center ++ // this must be done last so that the client does not ignore any of our unload chunk packets ++ if (needsChunkCenterUpdate) { ++ this.player.connection.send(new ClientboundSetChunkCacheCenterPacket(centerChunkX, centerChunkZ)); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.entity; ++ ++import com.destroystokyo.paper.util.maplist.EntityList; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.ChunkSystem; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.level.entity.EntityInLevelCallback; ++import net.minecraft.world.level.entity.EntityTypeTest; ++import net.minecraft.world.level.entity.LevelCallback; ++import net.minecraft.world.level.entity.LevelEntityGetter; ++import net.minecraft.world.level.entity.Visibility; ++import net.minecraft.world.phys.AABB; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.NoSuchElementException; ++import java.util.UUID; ++import java.util.concurrent.locks.StampedLock; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++ ++public final class EntityLookup implements LevelEntityGetter { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected static final int REGION_SHIFT = 5; ++ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; ++ protected static final int REGION_SIZE = 1 << REGION_SHIFT; ++ ++ public final ServerLevel world; ++ ++ private final StampedLock stateLock = new StampedLock(); ++ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(128, 0.5f); ++ ++ private final int minSection; // inclusive ++ private final int maxSection; // inclusive ++ private final LevelCallback worldCallback; ++ ++ private final StampedLock entityByLock = new StampedLock(); ++ private final Int2ReferenceOpenHashMap entityById = new Int2ReferenceOpenHashMap<>(); ++ private final Object2ReferenceOpenHashMap entityByUUID = new Object2ReferenceOpenHashMap<>(); ++ private final EntityList accessibleEntities = new EntityList(); ++ ++ public EntityLookup(final ServerLevel world, final LevelCallback worldCallback) { ++ this.world = world; ++ this.minSection = WorldUtil.getMinSection(world); ++ this.maxSection = WorldUtil.getMaxSection(world); ++ this.worldCallback = worldCallback; ++ } ++ ++ private static Entity maskNonAccessible(final Entity entity) { ++ if (entity == null) { ++ return null; ++ } ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ return visibility.isAccessible() ? entity : null; ++ } ++ ++ @Nullable ++ @Override ++ public Entity get(final int id) { ++ final long attempt = this.entityByLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final Entity ret = this.entityById.get(id); ++ ++ if (this.entityByLock.validate(attempt)) { ++ return maskNonAccessible(ret); ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.entityByLock.readLock(); ++ try { ++ return maskNonAccessible(this.entityById.get(id)); ++ } finally { ++ this.entityByLock.tryUnlockRead(); ++ } ++ } ++ ++ @Nullable ++ @Override ++ public Entity get(final UUID id) { ++ final long attempt = this.entityByLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final Entity ret = this.entityByUUID.get(id); ++ ++ if (this.entityByLock.validate(attempt)) { ++ return maskNonAccessible(ret); ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.entityByLock.readLock(); ++ try { ++ return maskNonAccessible(this.entityByUUID.get(id)); ++ } finally { ++ this.entityByLock.tryUnlockRead(); ++ } ++ } ++ ++ public boolean hasEntity(final UUID uuid) { ++ return this.get(uuid) != null; ++ } ++ ++ public String getDebugInfo() { ++ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size(); ++ } ++ ++ static final class ArrayIterable implements Iterable { ++ ++ private final T[] array; ++ private final int off; ++ private final int length; ++ ++ public ArrayIterable(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ if (length > array.length) { ++ throw new IllegalArgumentException("Length must be no greater-than the array length"); ++ } ++ } ++ ++ @NotNull ++ @Override ++ public Iterator iterator() { ++ return new ArrayIterator<>(this.array, this.off, this.length); ++ } ++ ++ static final class ArrayIterator implements Iterator { ++ ++ private final T[] array; ++ private int off; ++ private final int length; ++ ++ public ArrayIterator(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ return this.off < this.length; ++ } ++ ++ @Override ++ public T next() { ++ if (this.off >= this.length) { ++ throw new NoSuchElementException(); ++ } ++ return this.array[this.off++]; ++ } ++ ++ @Override ++ public void remove() { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ } ++ ++ @Override ++ public Iterable getAll() { ++ return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size()); ++ } ++ ++ @Override ++ public void get(final EntityTypeTest filter, final Consumer action) { ++ for (final Entity entity : this.entityById.values()) { ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ if (!visibility.isAccessible()) { ++ continue; ++ } ++ final U casted = filter.tryCast(entity); ++ if (casted != null) { ++ action.accept(casted); ++ } ++ } ++ } ++ ++ @Override ++ public void get(final AABB box, final Consumer action) { ++ List entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ action.accept(entities.get(i)); ++ } ++ } ++ ++ @Override ++ public void get(final EntityTypeTest filter, final AABB box, final Consumer action) { ++ List entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final U casted = filter.tryCast(entities.get(i)); ++ if (casted != null) { ++ action.accept(casted); ++ } ++ } ++ } ++ ++ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, ++ final boolean created, final boolean destroyed) { ++ TickThread.ensureTickThread(entity, "Entity status change must only happen on the main thread"); ++ ++ if (entity.updatingSectionStatus) { ++ // recursive status update ++ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); ++ return; ++ } ++ ++ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); ++ ++ if (entityStatusUpdateBefore) { ++ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); ++ return; ++ } ++ ++ try { ++ final Boolean ticketBlockBefore = this.world.chunkTaskScheduler.chunkHolderManager.blockTicketUpdates(); ++ try { ++ entity.updatingSectionStatus = true; ++ try { ++ if (created) { ++ EntityLookup.this.worldCallback.onCreated(entity); ++ } ++ ++ if (oldVisibility == newVisibility) { ++ if (moved && newVisibility.isAccessible()) { ++ EntityLookup.this.worldCallback.onSectionChange(entity); ++ } ++ return; ++ } ++ ++ if (newVisibility.ordinal() > oldVisibility.ordinal()) { ++ // status upgrade ++ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { ++ this.accessibleEntities.add(entity); ++ EntityLookup.this.worldCallback.onTrackingStart(entity); ++ } ++ ++ if (!oldVisibility.isTicking() && newVisibility.isTicking()) { ++ EntityLookup.this.worldCallback.onTickingStart(entity); ++ } ++ } else { ++ // status downgrade ++ if (oldVisibility.isTicking() && !newVisibility.isTicking()) { ++ EntityLookup.this.worldCallback.onTickingEnd(entity); ++ } ++ ++ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { ++ this.accessibleEntities.remove(entity); ++ EntityLookup.this.worldCallback.onTrackingEnd(entity); ++ } ++ } ++ ++ if (moved && newVisibility.isAccessible()) { ++ EntityLookup.this.worldCallback.onSectionChange(entity); ++ } ++ ++ if (destroyed) { ++ EntityLookup.this.worldCallback.onDestroyed(entity); ++ } ++ } finally { ++ entity.updatingSectionStatus = false; ++ } ++ } finally { ++ this.world.chunkTaskScheduler.chunkHolderManager.unblockTicketUpdates(ticketBlockBefore); ++ } ++ } finally { ++ if (slices != null) { ++ slices.stopPreventingStatusUpdates(false); ++ } ++ } ++ } ++ ++ public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) { ++ this.getChunk(x, z).updateStatus(newStatus, this); ++ } ++ ++ public void addLegacyChunkEntities(final List entities) { ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ this.addEntity(entities.get(i), true); ++ } ++ } ++ ++ public void addEntityChunkEntities(final List entities) { ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ this.addEntity(entities.get(i), true); ++ } ++ } ++ ++ public void addWorldGenChunkEntities(final List entities) { ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ this.addEntity(entities.get(i), false); ++ } ++ } ++ ++ public boolean addNewEntity(final Entity entity) { ++ return this.addEntity(entity, false); ++ } ++ ++ public static Visibility getEntityStatus(final Entity entity) { ++ if (entity.isAlwaysTicking()) { ++ return Visibility.TICKING; ++ } ++ final ChunkHolder.FullChunkStatus entityStatus = entity.chunkStatus; ++ return Visibility.fromFullChunkStatus(entityStatus == null ? ChunkHolder.FullChunkStatus.INACCESSIBLE : entityStatus); ++ } ++ ++ private boolean addEntity(final Entity entity, final boolean fromDisk) { ++ final BlockPos pos = entity.blockPosition(); ++ final int sectionX = pos.getX() >> 4; ++ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); ++ final int sectionZ = pos.getZ() >> 4; ++ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot add entity off-main thread"); ++ ++ if (entity.isRemoved()) { ++ LOGGER.warn("Refusing to add removed entity: " + entity); ++ return false; ++ } ++ ++ if (entity.updatingSectionStatus) { ++ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); ++ return false; ++ } ++ ++ if (fromDisk) { ++ ChunkSystem.onEntityPreAdd(this.world, entity); ++ if (entity.isRemoved()) { ++ // removed from checkDupeUUID call ++ return false; ++ } ++ } ++ ++ this.entityByLock.writeLock(); ++ try { ++ if (this.entityById.containsKey(entity.getId())) { ++ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + this.entityById.get(entity.getId()) + ", can't add " + entity, new Throwable()); ++ return false; ++ } ++ if (this.entityByUUID.containsKey(entity.getUUID())) { ++ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + this.entityByUUID.get(entity.getUUID()) + ", can't add " + entity, new Throwable()); ++ return false; ++ } ++ this.entityById.put(entity.getId(), entity); ++ this.entityByUUID.put(entity.getUUID(), entity); ++ } finally { ++ this.entityByLock.tryUnlockWrite(); ++ } ++ ++ entity.sectionX = sectionX; ++ entity.sectionY = sectionY; ++ entity.sectionZ = sectionZ; ++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); ++ if (!slices.addEntity(entity, sectionY)) { ++ LOGGER.warn("Entity " + entity + " added to world '" + this.world.getWorld().getName() + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); ++ } ++ ++ entity.setLevelCallback(new EntityCallback(entity)); ++ ++ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); ++ ++ return true; ++ } ++ ++ private void removeEntity(final Entity entity) { ++ final int sectionX = entity.sectionX; ++ final int sectionY = entity.sectionY; ++ final int sectionZ = entity.sectionZ; ++ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot remove entity off-main"); ++ if (!entity.isRemoved()) { ++ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); ++ } ++ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); ++ // all entities should be in a chunk ++ if (slices == null) { ++ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); ++ } else { ++ if (!slices.removeEntity(entity, sectionY)) { ++ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); ++ } ++ } ++ entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE; ++ ++ this.entityByLock.writeLock(); ++ try { ++ if (!this.entityById.remove(entity.getId(), entity)) { ++ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + this.entityById.get(entity.getId())); ++ } ++ if (!this.entityByUUID.remove(entity.getUUID(), entity)) { ++ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + this.entityByUUID.get(entity.getUUID())); ++ } ++ } finally { ++ this.entityByLock.tryUnlockWrite(); ++ } ++ } ++ ++ private ChunkEntitySlices moveEntity(final Entity entity) { ++ // ensure we own the entity ++ TickThread.ensureTickThread(entity, "Cannot move entity off-main"); ++ ++ final BlockPos newPos = entity.blockPosition(); ++ final int newSectionX = newPos.getX() >> 4; ++ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); ++ final int newSectionZ = newPos.getZ() >> 4; ++ ++ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { ++ return null; ++ } ++ ++ // ensure the new section is owned by this tick thread ++ TickThread.ensureTickThread(this.world, newSectionX, newSectionZ, "Cannot move entity off-main"); ++ ++ // ensure the old section is owned by this tick thread ++ TickThread.ensureTickThread(this.world, entity.sectionX, entity.sectionZ, "Cannot move entity off-main"); ++ ++ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); ++ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); ++ ++ if (!old.removeEntity(entity, entity.sectionY)) { ++ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + entity.sectionX + "," + entity.sectionY + "," + entity.sectionZ + ") since it was not contained in the section"); ++ } ++ ++ if (!slices.addEntity(entity, newSectionY)) { ++ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); ++ } ++ ++ entity.sectionX = newSectionX; ++ entity.sectionY = newSectionY; ++ entity.sectionZ = newSectionZ; ++ ++ return slices; ++ } ++ ++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getEntities(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getHardCollidingEntities(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ continue; ++ } ++ ++ chunk.getEntities(clazz, except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot load in entity section off-main"); ++ synchronized (this) { ++ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); ++ if (curr != null) { ++ this.removeChunk(chunkX, chunkZ); ++ ++ curr.mergeInto(slices); ++ ++ this.addChunk(chunkX, chunkZ, slices); ++ } else { ++ this.addChunk(chunkX, chunkZ, slices); ++ } ++ } ++ } ++ ++ public void entitySectionUnload(final int chunkX, final int chunkZ) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot unload entity section off-main"); ++ this.removeChunk(chunkX, chunkZ); ++ } ++ ++ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { ++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ if (region == null) { ++ return null; ++ } ++ ++ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); ++ } ++ ++ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { ++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ ChunkEntitySlices ret; ++ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { ++ // loadInEntityChunk will call addChunk for us ++ return this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(chunkX, chunkZ, true); ++ } ++ ++ return ret; ++ } ++ ++ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { ++ final long key = CoordinateUtils.getChunkKey(regionX, regionZ); ++ final long attempt = this.stateLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final ChunkSlicesRegion ret = this.regions.get(key); ++ ++ if (this.stateLock.validate(attempt)) { ++ return ret; ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.stateLock.readLock(); ++ try { ++ return this.regions.get(key); ++ } finally { ++ this.stateLock.tryUnlockRead(); ++ } ++ } ++ ++ private synchronized void removeChunk(final int chunkX, final int chunkZ) { ++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); ++ ++ final ChunkSlicesRegion region = this.regions.get(key); ++ final int remaining = region.remove(relIndex); ++ ++ if (remaining == 0) { ++ this.stateLock.writeLock(); ++ try { ++ this.regions.remove(key); ++ } finally { ++ this.stateLock.tryUnlockWrite(); ++ } ++ } ++ } ++ ++ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { ++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); ++ ++ ChunkSlicesRegion region = this.regions.get(key); ++ if (region != null) { ++ region.add(relIndex, slices); ++ } else { ++ region = new ChunkSlicesRegion(); ++ region.add(relIndex, slices); ++ this.stateLock.writeLock(); ++ try { ++ this.regions.put(key, region); ++ } finally { ++ this.stateLock.tryUnlockWrite(); ++ } ++ } ++ } ++ ++ public static final class ChunkSlicesRegion { ++ ++ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; ++ protected int sliceCount; ++ ++ public ChunkEntitySlices get(final int index) { ++ return this.slices[index]; ++ } ++ ++ public int remove(final int index) { ++ final ChunkEntitySlices slices = this.slices[index]; ++ if (slices == null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.slices[index] = null; ++ ++ return --this.sliceCount; ++ } ++ ++ public void add(final int index, final ChunkEntitySlices slices) { ++ final ChunkEntitySlices curr = this.slices[index]; ++ if (curr != null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.slices[index] = slices; ++ ++ ++this.sliceCount; ++ } ++ } ++ ++ private final class EntityCallback implements EntityInLevelCallback { ++ ++ public final Entity entity; ++ ++ public EntityCallback(final Entity entity) { ++ this.entity = entity; ++ } ++ ++ @Override ++ public void onMove() { ++ final Entity entity = this.entity; ++ final Visibility oldVisibility = getEntityStatus(entity); ++ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); ++ if (newSlices == null) { ++ // no new section, so didn't change sections ++ return; ++ } ++ final Visibility newVisibility = getEntityStatus(entity); ++ ++ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); ++ } ++ ++ @Override ++ public void onRemove(final Entity.RemovalReason reason) { ++ final Entity entity = this.entity; ++ TickThread.ensureTickThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system ++ final Visibility tickingState = EntityLookup.getEntityStatus(entity); ++ ++ EntityLookup.this.removeEntity(entity); ++ ++ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); ++ ++ this.entity.setLevelCallback(NoOpCallback.INSTANCE); ++ } ++ } ++ ++ private static final class NoOpCallback implements EntityInLevelCallback { ++ ++ public static final NoOpCallback INSTANCE = new NoOpCallback(); ++ ++ @Override ++ public void onMove() {} ++ ++ @Override ++ public void onRemove(final Entity.RemovalReason reason) {} ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.io; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.HashCommon; ++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 net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import org.slf4j.Logger; ++import java.io.IOException; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BiConsumer; ++import java.util.function.BiFunction; ++import java.util.function.Consumer; ++import java.util.function.Function; ++ ++/** ++ * Prioritised RegionFile I/O executor, responsible for all RegionFile access. ++ *

    ++ * All functions provided are MT-Safe, however certain ordering constraints are recommended: ++ *

  • ++ * Chunk saves may not occur for unloaded chunks. ++ *
  • ++ *
  • ++ * Tasks must be scheduled on the chunk scheduler thread. ++ *
  • ++ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. ++ *

    ++ */ ++public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ /** ++ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure ++ * getControllerFor is updated. ++ */ ++ public static enum RegionFileType { ++ CHUNK_DATA, ++ POI_DATA, ++ ENTITY_DATA; ++ } ++ ++ protected static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); ++ ++ private ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return world.chunkDataControllerNew; ++ case POI_DATA: ++ return world.poiDataControllerNew; ++ case ENTITY_DATA: ++ return world.entityDataControllerNew; ++ default: ++ throw new IllegalStateException("Unknown controller type " + type); ++ } ++ } ++ ++ /** ++ * Collects regionfile data for a certain chunk. ++ */ ++ public static final class RegionFileData { ++ ++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; ++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; ++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param data The result to set. ++ */ ++ public void setData(final RegionFileType type, final CompoundTag data) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.data[index] = data; ++ } ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param throwable The result to set. ++ */ ++ public void setThrowable(final RegionFileType type, final Throwable throwable) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.throwables[index] = throwable; ++ } ++ ++ /** ++ * Returns whether there is a result for the specified regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @return Whether a result exists for {@code type}. ++ */ ++ public boolean hasResult(final RegionFileType type) { ++ return this.hasResult[type.ordinal()]; ++ } ++ ++ /** ++ * Returns the data result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The data result for the specified type. If the result is a {@code Throwable}, ++ * then returns {@code null}. ++ */ ++ public CompoundTag getData(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.data[index]; ++ } ++ ++ /** ++ * Returns the throwable result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, ++ * then returns {@code null}. ++ */ ++ public Throwable getThrowable(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.throwables[index]; ++ } ++ } ++ ++ private static final Object INIT_LOCK = new Object(); ++ ++ static RegionFileIOThread[] threads; ++ ++ /* needs to be consistent given a set of parameters */ ++ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ if (threads == null) { ++ throw new IllegalStateException("Threads not initialised"); ++ } ++ ++ final int regionX = chunkX >> 5; ++ final int regionZ = chunkZ >> 5; ++ final int typeOffset = type.ordinal(); ++ ++ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; ++ } ++ ++ /** ++ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. ++ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. ++ * ++ * @param wait Whether to wait until all tasks have completed. ++ */ ++ public static void close(final boolean wait) { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].close(false, true); ++ } ++ if (wait) { ++ RegionFileIOThread.flush(); ++ } ++ } ++ ++ public static long[] getExecutedTasks() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksExecuted(); ++ } ++ ++ return ret; ++ } ++ ++ public static long[] getTasksScheduled() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksScheduled(); ++ } ++ return ret; ++ } ++ ++ public static void flush() { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].waitUntilAllExecuted(); ++ } ++ } ++ ++ public static void partialFlush(final int totalTasksRemaining) { ++ long failures = 1L; // start out at 0.25ms ++ ++ for (;;) { ++ final long[] executed = getExecutedTasks(); ++ final long[] scheduled = getTasksScheduled(); ++ ++ long sum = 0; ++ for (int i = 0; i < executed.length; ++i) { ++ sum += scheduled[i] - executed[i]; ++ } ++ ++ if (sum <= totalTasksRemaining) { ++ break; ++ } ++ ++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms ++ } ++ } ++ ++ /** ++ * Inits the executor with the specified number of threads. ++ * ++ * @param threads Specified number of threads. ++ */ ++ public static void init(final int threads) { ++ synchronized (INIT_LOCK) { ++ if (RegionFileIOThread.threads != null) { ++ throw new IllegalStateException("Already initialised threads"); ++ } ++ ++ RegionFileIOThread.threads = new RegionFileIOThread[threads]; ++ ++ for (int i = 0; i < threads; ++i) { ++ RegionFileIOThread.threads[i] = new RegionFileIOThread(i); ++ RegionFileIOThread.threads[i].start(); ++ } ++ } ++ } ++ ++ private RegionFileIOThread(final int threadNumber) { ++ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time ++ this.setName("RegionFile I/O Thread #" + threadNumber); ++ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us ++ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { ++ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); ++ }); ++ } ++ ++ /** ++ * Returns whether the current thread is a regionfile I/O executor. ++ * @return Whether the current thread is a regionfile I/O executor. ++ */ ++ public static boolean isRegionFileThread() { ++ return Thread.currentThread() instanceof RegionFileIOThread; ++ } ++ ++ /** ++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid ++ * dumb plugins from taking away priority from threads we consider crucial. ++ * @return The priroity to use with blocking I/O on the current thread. ++ */ ++ public static PrioritisedExecutor.Priority getIOBlockingPriorityForCurrentThread() { ++ if (TickThread.isTickThread()) { ++ return PrioritisedExecutor.Priority.BLOCKING; ++ } ++ return PrioritisedExecutor.Priority.HIGHEST; ++ } ++ ++ /** ++ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type. ++ * Note that this does not copy the result, so do not modify the result returned. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * ++ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. ++ */ ++ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); ++ } ++ ++ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task == null) { ++ return null; ++ } ++ ++ final CompoundTag ret = task.inProgressWrite; ++ ++ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; ++ } ++ ++ /** ++ * Returns the priority for the specified regionfile type for the specified chunk. ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @return The priority for the chunk ++ */ ++ public static PrioritisedExecutor.Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPriorityInternal(world, chunkX, chunkZ, type); ++ } ++ ++ PrioritisedExecutor.Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task == null) { ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ ++ return task.prioritisedTask.getPriority(); ++ } ++ ++ /** ++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.setPriority(priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.lowerPriority(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 data Chunk's data ++ * @param type The regionfile type to write to. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type) { ++ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * 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 data Chunk's data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); ++ } ++ ++ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ ++ final boolean[] created = new boolean[1]; ++ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ final ChunkDataTask task = taskController.tasks.compute(key, (final ChunkCoordinate keyInMap, final ChunkDataTask taskRunning) -> { ++ if (taskRunning == null || taskRunning.failedWrite) { ++ // no task is scheduled or the previous write failed - meaning we need to overwrite it ++ ++ // create task ++ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); ++ newTask.inProgressWrite = data; ++ created[0] = true; ++ ++ return newTask; ++ } ++ ++ taskRunning.inProgressWrite = data; ++ ++ return taskRunning; ++ }); ++ ++ if (created[0]) { ++ task.prioritisedTask.queue(); ++ } else { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock) { ++ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final PrioritisedExecutor.Priority priority) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final RegionFileType... types) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL, types); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final PrioritisedExecutor.Priority priority, final RegionFileType... types) { ++ if (types == null) { ++ throw new NullPointerException("Types cannot be null"); ++ } ++ if (types.length == 0) { ++ throw new IllegalArgumentException("Types cannot be empty"); ++ } ++ ++ final RegionFileData ret = new RegionFileData(); ++ ++ final Cancellable[] reads = new CancellableRead[types.length]; ++ final AtomicInteger completions = new AtomicInteger(); ++ final int expectedCompletions = types.length; ++ ++ for (int i = 0; i < expectedCompletions; ++i) { ++ final RegionFileType type = types[i]; ++ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, ++ (final CompoundTag data, final Throwable throwable) -> { ++ if (throwable != null) { ++ ret.setThrowable(type, throwable); ++ } else { ++ ret.setData(type, data); ++ } ++ ++ if (completions.incrementAndGet() == expectedCompletions) { ++ onComplete.accept(ret); ++ } ++ }, intendingToBlock, priority); ++ } ++ ++ return new CancellableReads(reads); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock) { ++ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ *

    ++ * Impl notes: ++ *

    ++ *
  • ++ * 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 onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); ++ } ++ ++ private static Boolean doesRegionFileExist(final int chunkX, final int chunkZ, final boolean intendingToBlock, ++ final ChunkDataController taskController) { ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ if (intendingToBlock) { ++ return taskController.computeForRegionFile(chunkX, chunkZ, true, (final RegionFile file) -> { ++ if (file == null) { // null if no regionfile exists ++ return Boolean.FALSE; ++ } ++ ++ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; ++ }); ++ } else { ++ return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final RegionFile file) -> { ++ if (file == null) { // null if not loaded ++ return Boolean.TRUE; ++ } ++ ++ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; ++ }); ++ } ++ } ++ ++ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ ++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); ++ ++ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ final BiFunction compute = (final ChunkCoordinate keyInMap, final ChunkDataTask running) -> { ++ if (running == null) { ++ // not scheduled ++ ++ if (callbackInfo.regionFileCalculation == null) { ++ // caller will compute this outside of compute(), to avoid holding the bin lock ++ callbackInfo.needsRegionFileTest = true; ++ return null; ++ } ++ ++ if (callbackInfo.regionFileCalculation == Boolean.FALSE) { ++ // not on disk ++ callbackInfo.data = null; ++ callbackInfo.throwable = null; ++ callbackInfo.completeNow = true; ++ return null; ++ } ++ ++ // set up task ++ final ChunkDataTask newTask = new ChunkDataTask( ++ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority ++ ); ++ newTask.inProgressRead = new RegionFileIOThread.InProgressRead(); ++ newTask.inProgressRead.waiters.add(onComplete); ++ ++ callbackInfo.tasksNeedsScheduling = true; ++ return newTask; ++ } ++ ++ final CompoundTag pendingWrite = running.inProgressWrite; ++ ++ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { ++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations ++ if (!running.inProgressRead.addToWaiters(onComplete)) { ++ callbackInfo.data = running.inProgressRead.value; ++ callbackInfo.throwable = running.inProgressRead.throwable; ++ callbackInfo.completeNow = true; ++ } ++ return running; ++ } ++ // using the result sync here - don't bump priority ++ ++ // at this stage we have to use the in progress write's data to avoid an order issue ++ callbackInfo.data = pendingWrite; ++ callbackInfo.throwable = null; ++ callbackInfo.completeNow = true; ++ return running; ++ }; ++ ++ ChunkDataTask curr = taskController.tasks.get(key); ++ if (curr == null) { ++ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); ++ } ++ ChunkDataTask ret = taskController.tasks.compute(key, compute); ++ if (callbackInfo.needsRegionFileTest) { ++ // curr isn't null but when we went into compute() it was ++ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); ++ // now it should be fine ++ ret = taskController.tasks.compute(key, compute); ++ } ++ ++ // needs to be scheduled ++ if (callbackInfo.tasksNeedsScheduling) { ++ ret.prioritisedTask.queue(); ++ } else if (callbackInfo.completeNow) { ++ try { ++ onComplete.accept(callbackInfo.data, callbackInfo.throwable); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); ++ } ++ } else { ++ // we're waiting on a task we didn't schedule, so raise its priority to what we want ++ ret.prioritisedTask.raisePriority(priority); ++ } ++ ++ return new CancellableRead(onComplete, ret); ++ } ++ ++ /** ++ * Schedules a load task to be executed asynchronously, and blocks on that task. ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param type Regionfile type ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. ++ * ++ * @throws IOException If the load fails for any reason ++ */ ++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) throws IOException { ++ final CompletableFuture ret = new CompletableFuture<>(); ++ ++ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(compound); ++ } ++ }, true, priority); ++ ++ try { ++ return ret.join(); ++ } catch (final CompletionException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ private static final class ImmediateCallbackCompletion { ++ ++ public CompoundTag data; ++ public Throwable throwable; ++ public boolean completeNow; ++ public boolean tasksNeedsScheduling; ++ public boolean needsRegionFileTest; ++ public Boolean regionFileCalculation; ++ ++ } ++ ++ static final class CancellableRead implements Cancellable { ++ ++ private BiConsumer callback; ++ private RegionFileIOThread.ChunkDataTask task; ++ ++ CancellableRead(final BiConsumer callback, final RegionFileIOThread.ChunkDataTask task) { ++ this.callback = callback; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer callback = this.callback; ++ final RegionFileIOThread.ChunkDataTask task = this.task; ++ ++ if (callback == null || task == null) { ++ return false; ++ } ++ ++ this.callback = null; ++ this.task = null; ++ ++ final RegionFileIOThread.InProgressRead read = task.inProgressRead; ++ ++ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) ++ return (read != null && read.waiters.remove(callback)); ++ } ++ } ++ ++ static final class CancellableReads implements Cancellable { ++ ++ private Cancellable[] reads; ++ ++ protected static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); ++ ++ CancellableReads(final Cancellable[] reads) { ++ this.reads = reads; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); ++ ++ if (reads == null) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final Cancellable read : reads) { ++ ret |= read.cancel(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ static final class InProgressRead { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ CompoundTag value; ++ Throwable throwable; ++ final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); ++ ++ // rets false if already completed (callback not invoked), true if callback was added ++ boolean addToWaiters(final BiConsumer callback) { ++ return this.waiters.add(callback); ++ } ++ ++ void complete(final RegionFileIOThread.ChunkDataTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ ++ BiConsumer consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(value, throwable); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Class exists to replace {@link Long} usages as keys inside non-fastutil hashtables. The hash for some Long {@code x} ++ * is defined as {@code (x >>> 32) ^ x}. Chunk keys as long values are defined as {@code ((chunkX & 0xFFFFFFFFL) | (chunkZ << 32))}, ++ * which means the hashcode as a Long value will be {@code chunkX ^ chunkZ}. Given that most chunks are created within a radius arounds players, ++ * this will lead to many hash collisions. So, this class uses a better hashing algorithm so that usage of ++ * non-fastutil collections is not degraded. ++ */ ++ public static final class ChunkCoordinate implements Comparable { ++ ++ public final long key; ++ ++ public ChunkCoordinate(final long key) { ++ this.key = key; ++ } ++ ++ @Override ++ public int hashCode() { ++ return (int)HashCommon.mix(this.key); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof ChunkCoordinate)) { ++ return false; ++ } ++ ++ final ChunkCoordinate other = (ChunkCoordinate)obj; ++ ++ return this.key == other.key; ++ } ++ ++ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain ++ // is too large. So we should implement compareTo to help. ++ @Override ++ public int compareTo(final RegionFileIOThread.ChunkCoordinate other) { ++ return Long.compare(this.key, other.key); ++ } ++ ++ @Override ++ public String toString() { ++ return new ChunkPos(this.key).toString(); ++ } ++ } ++ ++ public static abstract class ChunkDataController { ++ ++ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. ++ protected final ConcurrentHashMap tasks = new ConcurrentHashMap<>(8192, 0.10f); ++ ++ public final RegionFileType type; ++ ++ public ChunkDataController(final RegionFileType type) { ++ this.type = type; ++ } ++ ++ public abstract RegionFileStorage getCache(); ++ ++ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; ++ ++ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; ++ ++ public boolean hasTasks() { ++ return !this.tasks.isEmpty(); ++ } ++ ++ public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ synchronized (cache) { ++ try { ++ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly, true); ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++ ++ try { ++ return function.apply(regionFile); ++ } finally { ++ if (regionFile != null) { ++ regionFile.fileLock.unlock(); ++ } ++ } ++ } ++ ++ public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ ++ synchronized (cache) { ++ regionFile = cache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ)); ++ if (regionFile != null) { ++ regionFile.fileLock.lock(); ++ } ++ } ++ ++ try { ++ return function.apply(regionFile); ++ } finally { ++ if (regionFile != null) { ++ regionFile.fileLock.unlock(); ++ } ++ } ++ } ++ } ++ ++ static final class ChunkDataTask implements Runnable { ++ ++ protected static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ RegionFileIOThread.InProgressRead inProgressRead; ++ volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release ++ ++ boolean failedWrite; ++ ++ final ServerLevel world; ++ final int chunkX; ++ final int chunkZ; ++ final RegionFileIOThread.ChunkDataController taskController; ++ ++ final PrioritisedExecutor.PrioritisedTask prioritisedTask; ++ ++ /* ++ * IO thread will perform reads before writes for a given chunk x and z ++ * ++ * How reads/writes are scheduled: ++ * ++ * If read is scheduled while scheduling write, take no special action and just schedule write ++ * If read is scheduled while scheduling read and no write is scheduled, chain the read task ++ * ++ * ++ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) ++ * If write is scheduled 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 thanks to writes overwriting each other ++ */ ++ ++ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileIOThread.ChunkDataController taskController, ++ final PrioritisedExecutor executor, final PrioritisedExecutor.Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.taskController = taskController; ++ this.prioritisedTask = executor.createTask(this, priority); ++ } ++ ++ @Override ++ public String toString() { ++ return "Task for world: '" + this.world.getWorld().getName() + "' at (" + this.chunkX + "," + this.chunkZ + ++ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); ++ } ++ ++ @Override ++ public void run() { ++ final RegionFileIOThread.InProgressRead read = this.inProgressRead; ++ final ChunkCoordinate chunkKey = new ChunkCoordinate(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ ++ if (read != null) { ++ final boolean[] canRead = new boolean[] { true }; ++ ++ if (read.waiters.isEmpty()) { ++ // cancelled read? go to task controller to confirm ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate 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 (!read.waiters.isEmpty()) { // as per usual IntelliJ is unable to figure out that there are concurrent accesses. ++ return valueInMap; ++ } else { ++ canRead[0] = false; ++ } ++ ++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ // read is cancelled - and no write pending, so we're done ++ return; ++ } ++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - ++ // the readers will just use the in progress write, so the value in canRead is good to use without ++ // further synchronisation. ++ } ++ ++ if (canRead[0]) { ++ CompoundTag compound = null; ++ Throwable throwable = null; ++ ++ try { ++ compound = this.taskController.readData(this.chunkX, this.chunkZ); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); ++ } ++ read.complete(this, compound, throwable); ++ } ++ } ++ ++ CompoundTag write = this.inProgressWrite; ++ ++ if (write == NOTHING_TO_WRITE) { ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate 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 == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ return; // set the task value to null, indicating we're done ++ } // else: inProgressWrite changed, so now we have something to write ++ } ++ ++ for (;;) { ++ write = this.inProgressWrite; ++ final CompoundTag dataWritten = write; ++ ++ boolean failedWrite = false; ++ ++ try { ++ this.taskController.writeData(this.chunkX, this.chunkZ, write); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ if (thr instanceof RegionFileStorage.RegionFileSizeException) { ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ } else { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ } ++ ++ final boolean finalFailWrite = failedWrite; ++ final boolean[] done = new boolean[] { false }; ++ ++ this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate 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 == dataWritten) { ++ valueInMap.failedWrite = finalFailWrite; ++ done[0] = true; ++ // keep the data in map if we failed the write so we can try to prevent data loss ++ return finalFailWrite ? valueInMap : null; ++ } ++ // different data than expected, means we need to retry write ++ return valueInMap; ++ }); ++ ++ if (done[0]) { ++ return; ++ } ++ ++ // fetch & write new data ++ continue; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.light; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.starlight.common.light.BlockStarLightEngine; ++import ca.spottedleaf.starlight.common.light.SkyStarLightEngine; ++import ca.spottedleaf.starlight.common.light.StarLightInterface; ++import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.BooleanSupplier; ++ ++public final class LightQueue { ++ ++ protected final Long2ObjectOpenHashMap chunkTasks = new Long2ObjectOpenHashMap<>(); ++ protected final StarLightInterface manager; ++ protected final ServerLevel world; ++ ++ public LightQueue(final StarLightInterface manager) { ++ this.manager = manager; ++ this.world = ((ServerLevel)manager.getWorld()); ++ } ++ ++ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ return task.getPriority(); ++ } ++ ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ ++ public boolean isEmpty() { ++ synchronized (this) { ++ return this.chunkTasks.isEmpty(); ++ } ++ } ++ ++ public CompletableFuture queueBlockChange(final BlockPos pos) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ tasks.changedPositions.add(pos.immutable()); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks.onComplete; ++ } ++ ++ public CompletableFuture queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ if (tasks.changedSectionSet == null) { ++ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; ++ } ++ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks.onComplete; ++ } ++ ++ public CompletableFuture queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this, priority); ++ }); ++ if (tasks.lightTasks == null) { ++ tasks.lightTasks = new ArrayList<>(); ++ } ++ tasks.lightTasks.add(lightTask); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks.onComplete; ++ } ++ ++ public CompletableFuture queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks.onComplete; ++ } ++ ++ public CompletableFuture queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks; ++ ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks.onComplete; ++ } ++ ++ public void removeChunk(final ChunkPos pos) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); ++ } ++ if (tasks != null && tasks.cancel()) { ++ tasks.onComplete.complete(null); ++ } ++ } ++ ++ protected static final class ChunkTasks implements Runnable { ++ ++ final Set changedPositions = new HashSet<>(); ++ Boolean[] changedSectionSet; ++ ShortOpenHashSet queuedEdgeChecksSky; ++ ShortOpenHashSet queuedEdgeChecksBlock; ++ List lightTasks; ++ ++ final CompletableFuture onComplete = new CompletableFuture<>(); ++ ++ public final long chunkCoordinate; ++ private final StarLightInterface lightEngine; ++ private final LightQueue queue; ++ private final PrioritisedExecutor.PrioritisedTask task; ++ ++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { ++ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue, ++ final PrioritisedExecutor.Priority priority) { ++ this.chunkCoordinate = chunkCoordinate; ++ this.lightEngine = lightEngine; ++ this.queue = queue; ++ this.task = queue.world.chunkTaskScheduler.lightExecutor.createTask(this, priority); ++ } ++ ++ public void schedule() { ++ this.task.queue(); ++ } ++ ++ public boolean cancel() { ++ return this.task.cancel(); ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.task.getPriority(); ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.lowerPriority(priority); ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.setPriority(priority); ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.task.raisePriority(priority); ++ } ++ ++ @Override ++ public void run() { ++ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); ++ try { ++ synchronized (this.queue) { ++ this.queue.chunkTasks.remove(this.chunkCoordinate); ++ } ++ ++ boolean litChunk = false; ++ if (this.lightTasks != null) { ++ for (final BooleanSupplier run : this.lightTasks) { ++ if (run.getAsBoolean()) { ++ litChunk = true; ++ break; ++ } ++ } ++ } ++ ++ final long coordinate = this.chunkCoordinate; ++ final int chunkX = CoordinateUtils.getChunkX(coordinate); ++ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); ++ ++ final Set positions = this.changedPositions; ++ final Boolean[] sectionChanges = this.changedSectionSet; ++ ++ if (!litChunk) { ++ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ ++ if (skyEngine != null && this.queuedEdgeChecksSky != null) { ++ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); ++ } ++ if (blockEngine != null && this.queuedEdgeChecksBlock != null) { ++ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); ++ } ++ } ++ ++ this.onComplete.complete(null); ++ } finally { ++ this.lightEngine.releaseSkyLightEngine(skyEngine); ++ this.lightEngine.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.poi; ++ ++import com.mojang.logging.LogUtils; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import net.minecraft.SharedConstants; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.ai.village.poi.PoiManager; ++import net.minecraft.world.entity.ai.village.poi.PoiSection; ++import org.slf4j.Logger; ++ ++import java.util.Optional; ++ ++public final class PoiChunk { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public final ServerLevel world; ++ public final int chunkX; ++ public final int chunkZ; ++ public final int minSection; ++ public final int maxSection; ++ ++ protected final PoiSection[] sections; ++ ++ private boolean isDirty; ++ private boolean loaded; ++ ++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) { ++ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]); ++ } ++ ++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.minSection = minSection; ++ this.maxSection = maxSection; ++ this.sections = sections; ++ if (this.sections.length != (maxSection - minSection + 1)) { ++ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length); ++ } ++ } ++ ++ public void load() { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main"); ++ if (this.loaded) { ++ return; ++ } ++ this.loaded = true; ++ this.world.chunkSource.getPoiManager().loadInPoiChunk(this); ++ } ++ ++ public boolean isLoaded() { ++ return this.loaded; ++ } ++ ++ public boolean isEmpty() { ++ for (final PoiSection section : this.sections) { ++ if (section != null && !section.isEmpty()) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ public PoiSection getOrCreateSection(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ final int idx = chunkY - this.minSection; ++ final PoiSection ret = this.sections[idx]; ++ if (ret != null) { ++ return ret; ++ } ++ ++ final PoiManager poiManager = this.world.getPoiManager(); ++ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ); ++ ++ return this.sections[idx] = new PoiSection(() -> { ++ poiManager.setDirty(key); ++ }); ++ } ++ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]"); ++ } ++ ++ public PoiSection getSection(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ return this.sections[chunkY - this.minSection]; ++ } ++ return null; ++ } ++ ++ public Optional getSectionForVanilla(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ final PoiSection ret = this.sections[chunkY - this.minSection]; ++ return ret == null ? Optional.empty() : ret.noAllocateOptional; ++ } ++ return Optional.empty(); ++ } ++ ++ public boolean isDirty() { ++ return this.isDirty; ++ } ++ ++ public void setDirty(final boolean dirty) { ++ this.isDirty = dirty; ++ } ++ ++ // returns null if empty ++ public CompoundTag save() { ++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); ++ ++ final CompoundTag ret = new CompoundTag(); ++ final CompoundTag sections = new CompoundTag(); ++ ret.put("Sections", sections); ++ ++ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); ++ ++ final ServerLevel world = this.world; ++ final PoiManager poiManager = world.getPoiManager(); ++ final int chunkX = this.chunkX; ++ final int chunkZ = this.chunkZ; ++ ++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { ++ final PoiSection chunk = this.sections[sectionY - this.minSection]; ++ if (chunk == null || chunk.isEmpty()) { ++ continue; ++ } ++ ++ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ // codecs are honestly such a fucking disaster. What the fuck is this trash? ++ final Codec codec = PoiSection.codec(() -> { ++ poiManager.setDirty(key); ++ }); ++ ++ final DataResult serializedResult = codec.encodeStart(registryOps, chunk); ++ final int finalSectionY = sectionY; ++ final Tag serialized = serializedResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to serialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).orElse(null); ++ if (serialized == null) { ++ // failed, should be logged from the resultOrPartial ++ continue; ++ } ++ ++ sections.put(Integer.toString(sectionY), serialized); ++ } ++ ++ return sections.isEmpty() ? null : ret; ++ } ++ ++ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { ++ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); ++ ret.loaded = true; ++ return ret; ++ } ++ ++ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { ++ final PoiChunk ret = empty(world, chunkX, chunkZ); ++ ++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); ++ ++ final CompoundTag sections = data.getCompound("Sections"); ++ ++ if (sections.isEmpty()) { ++ // nothing to parse ++ return ret; ++ } ++ ++ final PoiManager poiManager = world.getPoiManager(); ++ ++ boolean readAnything = false; ++ ++ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { ++ final String key = Integer.toString(sectionY); ++ if (!sections.contains(key)) { ++ continue; ++ } ++ ++ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ // codecs are honestly such a fucking disaster. What the fuck is this trash? ++ final Codec codec = PoiSection.codec(() -> { ++ poiManager.setDirty(coordinateKey); ++ }); ++ ++ final CompoundTag section = sections.getCompound(key); ++ final DataResult deserializeResult = codec.parse(registryOps, section); ++ final int finalSectionY = sectionY; ++ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to deserialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).orElse(null); ++ ++ if (deserialized == null || deserialized.isEmpty()) { ++ // completely empty, no point in storing this ++ continue; ++ } ++ ++ readAnything = true; ++ ret.sections[sectionY - ret.minSection] = deserialized; ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++ ++public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected final NewChunkHolder chunkHolder; ++ protected final ChunkAccess fromChunk; ++ protected final PrioritisedExecutor.PrioritisedTask convertToFullTask; ++ ++ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.fromChunk = fromChunk; ++ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.FULL; ++ } ++ ++ @Override ++ public void run() { ++ // See Vanilla protoChunkToFullChunk for what this function should be doing ++ final LevelChunk chunk; ++ try { ++ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { ++ chunk = wrappedFull.getWrapped(); ++ } else { ++ final ServerLevel world = this.world; ++ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; ++ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { ++ ChunkMap.postLoadProtoChunk(world, protoChunk.getEntities()); ++ }); ++ } ++ ++ chunk.setChunkHolder(this.scheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ)); // replaces setFullStatus ++ chunk.runPostLoad(); ++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) ++ // This brings entity addition back in line with older versions of the game ++ // Since we load the NBT in the empty status, this will never block for I/O ++ this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); ++ ++ // we don't need the entitiesInLevel trash, this system doesn't double run callbacks ++ chunk.setLoaded(true); ++ chunk.registerAllBlockEntitiesAfterLevelLoad(); ++ chunk.registerTickContainerInLevel(this.world); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ this.complete(chunk, null); ++ } ++ ++ protected volatile boolean scheduled; ++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ this.convertToFullTask.queue(); ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.convertToFullTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.convertToFullTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.raisePriority(priority); ++ } ++} +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 +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import co.aikar.timings.Timing; ++import com.google.common.collect.ImmutableList; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2IntMap; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.ChunkSystem; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.Ticket; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.SortedArraySet; ++import net.minecraft.util.Unit; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import org.bukkit.plugin.Plugin; ++import org.slf4j.Logger; ++import java.io.IOException; ++import java.text.DecimalFormat; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Objects; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicReference; ++import java.util.concurrent.locks.LockSupport; ++import java.util.concurrent.locks.ReentrantLock; ++import java.util.function.Predicate; ++ ++public final class ChunkHolderManager { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final int FULL_LOADED_TICKET_LEVEL = 33; ++ public static final int BLOCK_TICKING_TICKET_LEVEL = 32; ++ public static final int ENTITY_TICKING_TICKET_LEVEL = 31; ++ public static final int MAX_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; // inclusive ++ ++ private static final long NO_TIMEOUT_MARKER = -1L; ++ ++ final ReentrantLock ticketLock = new ReentrantLock(); ++ ++ private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f); ++ private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f); ++ // what a disaster of a name ++ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick ++ private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); ++ private final ServerLevel world; ++ private final ChunkTaskScheduler taskScheduler; ++ private long currentTick; ++ ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } ++ ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ ++ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ this.world = world; ++ this.taskScheduler = taskScheduler; ++ } ++ ++ private long statusUpgradeId; ++ ++ long getNextStatusUpgradeId() { ++ return ++this.statusUpgradeId; ++ } ++ ++ public List getOldChunkHolders() { ++ final List holders = this.getChunkHolders(); ++ final List ret = new ArrayList<>(holders.size()); ++ for (final NewChunkHolder holder : holders) { ++ ret.add(holder.vanillaChunkHolder); ++ } ++ return ret; ++ } ++ ++ public List getChunkHolders() { ++ final List ret = new ArrayList<>(this.chunkHolders.size()); ++ this.chunkHolders.forEachValue(ret::add); ++ return ret; ++ } ++ ++ public int size() { ++ return this.chunkHolders.size(); ++ } ++ ++ public void close(final boolean save, final boolean halt) { ++ TickThread.ensureTickThread("Closing world off-main"); ++ if (halt) { ++ LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); ++ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'"); ++ } else { ++ LOGGER.info("Halted chunk system for world '" + this.world.getWorld().getName() + "'"); ++ } ++ } ++ ++ if (save) { ++ this.saveAllChunks(true, true, true); ++ } ++ ++ if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) { ++ RegionFileIOThread.flush(); ++ } ++ ++ // kill regionfile cache ++ try { ++ this.world.chunkDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close chunk regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ try { ++ this.world.entityDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close entity regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ try { ++ this.world.poiDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ } ++ ++ void ensureInAutosave(final NewChunkHolder holder) { ++ if (!this.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = MinecraftServer.currentTick; ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ ++ public void autoSave() { ++ final List reschedule = new ArrayList<>(); ++ final long currentTick = MinecraftServer.currentTickLong; ++ final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value(); ++ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { ++ final NewChunkHolder holder = this.autoSaveQueue.first(); ++ ++ if (holder.lastAutoSave > maxSaveTime) { ++ break; ++ } ++ ++ this.autoSaveQueue.remove(holder); ++ ++ holder.lastAutoSave = currentTick; ++ if (holder.save(false, false)) { ++ ++autoSaved; ++ } ++ ++ if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ reschedule.add(holder); ++ } ++ } ++ ++ for (final NewChunkHolder holder : reschedule) { ++ if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ } ++ ++ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { ++ final List holders = this.getChunkHolders(); ++ ++ if (logProgress) { ++ LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ final DecimalFormat format = new DecimalFormat("#0.00"); ++ ++ int saved = 0; ++ ++ long start = System.nanoTime(); ++ long lastLog = start; ++ boolean needsFlush = false; ++ final int flushInterval = 50; ++ ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ try { ++ if (holder.save(shutdown, false)) { ++ ++saved; ++ needsFlush = flush; ++ } ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); ++ } ++ if (needsFlush && (saved % flushInterval) == 0) { ++ needsFlush = false; ++ RegionFileIOThread.partialFlush(flushInterval / 2); ++ } ++ if (logProgress) { ++ final long currTime = System.nanoTime(); ++ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { ++ lastLog = currTime; ++ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + this.world.getWorld().getName() + "'"); ++ } ++ } ++ } ++ if (flush) { ++ RegionFileIOThread.flush(); ++ } ++ if (logProgress) { ++ LOGGER.info("Saved " + saved + " chunks in world '" + this.world.getWorld().getName() + "' in " + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) + "s"); ++ } ++ } ++ ++ protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() { ++ @Override ++ protected void rehash(final int newN) { ++ // no downsizing allowed ++ if (newN < this.n) { ++ return; ++ } ++ super.rehash(newN); ++ } ++ }; ++ ++ protected final Delayed8WayDistancePropagator2D ticketLevelPropagator = new Delayed8WayDistancePropagator2D( ++ (final long coordinate, final byte oldLevel, final byte newLevel) -> { ++ ChunkHolderManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel)); ++ } ++ ); ++ // function for converting between ticket levels and propagator levels and vice versa ++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects ++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator ++ // and the levels we get out of the propagator ++ ++ public static int convertBetweenTicketLevels(final int level) { ++ return ChunkMap.MAX_CHUNK_DISTANCE - level + 1; ++ } ++ ++ public boolean hasTickets() { ++ this.ticketLock.lock(); ++ try { ++ return !this.tickets.isEmpty(); ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public String getTicketDebugString(final long coordinate) { ++ this.ticketLock.lock(); ++ try { ++ final SortedArraySet> tickets = this.tickets.get(coordinate); ++ ++ return tickets != null ? tickets.first().toString() : "no_ticket"; ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public Long2ObjectOpenHashMap>> getTicketsCopy() { ++ this.ticketLock.lock(); ++ try { ++ return this.tickets.clone(); ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public Collection getPluginChunkTickets(int x, int z) { ++ ImmutableList.Builder ret; ++ this.ticketLock.lock(); ++ try { ++ SortedArraySet> tickets = this.tickets.get(ChunkPos.asLong(x, z)); ++ ++ if (tickets == null) { ++ return Collections.emptyList(); ++ } ++ ++ ret = ImmutableList.builder(); ++ for (Ticket ticket : tickets) { ++ if (ticket.getType() == TicketType.PLUGIN_TICKET) { ++ ret.add((Plugin)ticket.key); ++ } ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ ++ return ret.build(); ++ } ++ ++ protected final int getPropagatedTicketLevel(final long coordinate) { ++ return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate)); ++ } ++ ++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { ++ if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) { ++ this.ticketLevelPropagator.removeSource(coordinate); ++ } else { ++ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel)); ++ } ++ } ++ ++ private static int getTicketLevelAt(SortedArraySet> tickets) { ++ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; ++ } ++ ++ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ // supposed to return true if the ticket was added and did not replace another ++ // but, we always return false if the ticket cannot be added ++ public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ final long removeDelay = Math.max(0, type.timeout); ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ this.ticketLock.lock(); ++ try { ++ final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : this.currentTick + removeDelay; ++ final Ticket ticket = new Ticket<>(type, level, identifier, removeTick); ++ ++ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { ++ return SortedArraySet.create(4); ++ }); ++ ++ final int levelBefore = getTicketLevelAt(ticketsAtChunk); ++ final Ticket current = (Ticket)ticketsAtChunk.replace(ticket); ++ final int levelAfter = getTicketLevelAt(ticketsAtChunk); ++ ++ if (current != ticket) { ++ final long oldRemovalTick = current.removalTick; ++ if (removeTick != oldRemovalTick) { ++ if (oldRemovalTick != NO_TIMEOUT_MARKER) { ++ final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(oldRemovalTick); ++ final int prevCount = removeCounts.addTo(chunk, -1); ++ ++ if (prevCount == 1) { ++ removeCounts.remove(chunk); ++ if (removeCounts.isEmpty()) { ++ this.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); ++ } ++ } ++ } ++ if (removeTick != NO_TIMEOUT_MARKER) { ++ this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { ++ return new Long2IntOpenHashMap(); ++ }).addTo(chunk, 1); ++ } ++ } ++ } else { ++ if (removeTick != NO_TIMEOUT_MARKER) { ++ this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { ++ return new Long2IntOpenHashMap(); ++ }).addTo(chunk, 1); ++ } ++ } ++ ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunk, levelAfter); ++ } ++ ++ return current == ticket; ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ this.ticketLock.lock(); ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); ++ if (ticketsAtChunk == null) { ++ return false; ++ } ++ ++ final int oldLevel = getTicketLevelAt(ticketsAtChunk); ++ final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, -2L)); ++ ++ if (ticket == null) { ++ return false; ++ } ++ ++ if (ticketsAtChunk.isEmpty()) { ++ this.tickets.remove(chunk); ++ } ++ ++ final int newLevel = getTicketLevelAt(ticketsAtChunk); ++ ++ final long removeTick = ticket.removalTick; ++ if (removeTick != NO_TIMEOUT_MARKER) { ++ final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(removeTick); ++ final int currCount = removeCounts.addTo(chunk, -1); ++ ++ if (currCount == 1) { ++ removeCounts.remove(chunk); ++ if (removeCounts.isEmpty()) { ++ this.removeTickToChunkExpireTicketCount.remove(removeTick); ++ } ++ } ++ } ++ ++ if (oldLevel != newLevel) { ++ this.updateTicketLevel(chunk, newLevel); ++ } ++ ++ return true; ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ this.ticketLock.lock(); ++ try { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier); ++ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier); ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { ++ if (ticketLevel > MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ this.ticketLock.lock(); ++ try { ++ for (final LongIterator iterator = new LongArrayList(this.tickets.keySet()).longIterator(); iterator.hasNext();) { ++ final long chunk = iterator.nextLong(); ++ ++ this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ public void tick() { ++ TickThread.ensureTickThread("Cannot tick ticket manager off-main"); ++ ++ this.ticketLock.lock(); ++ try { ++ final long tick = ++this.currentTick; ++ ++ final Long2IntOpenHashMap toRemove = this.removeTickToChunkExpireTicketCount.remove(tick); ++ ++ if (toRemove == null) { ++ return; ++ } ++ ++ final Predicate> expireNow = (final Ticket ticket) -> { ++ return ticket.removalTick == tick; ++ }; ++ ++ for (final LongIterator iterator = toRemove.keySet().longIterator(); iterator.hasNext();) { ++ final long chunk = iterator.nextLong(); ++ ++ final SortedArraySet> tickets = this.tickets.get(chunk); ++ tickets.removeIf(expireNow); ++ if (tickets.isEmpty()) { ++ this.tickets.remove(chunk); ++ this.ticketLevelPropagator.removeSource(chunk); ++ } else { ++ this.ticketLevelPropagator.setSource(chunk, convertBetweenTicketLevels(tickets.first().getTicketLevel())); ++ } ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ ++ this.processTicketUpdates(); ++ } ++ ++ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { ++ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public NewChunkHolder getChunkHolder(final long position) { ++ return this.chunkHolders.get(position); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.raisePriority(priority); ++ } ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.setPriority(priority); ++ } ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.lowerPriority(priority); ++ } ++ } ++ ++ private NewChunkHolder createChunkHolder(final long position) { ++ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); ++ ++ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); ++ ret.vanillaChunkHolder.onChunkAdd(); ++ ++ return ret; ++ } ++ ++ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure ++ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, ++ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate ++ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { ++ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ private NewChunkHolder getOrCreateChunkHolder(final long position) { ++ if (!this.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Must hold ticket level update lock!"); ++ } ++ if (!this.taskScheduler.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Must hold scheduler lock!!"); ++ } ++ ++ // we could just acquire these locks, but... ++ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns ++ ++ NewChunkHolder current = this.chunkHolders.get(position); ++ if (current != null) { ++ return current; ++ } ++ ++ current = this.createChunkHolder(position); ++ this.chunkHolders.put(position, current); ++ ++ return current; ++ } ++ ++ private long entityLoadCounter; ++ ++ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); ++ ChunkEntitySlices ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ return ret; ++ } ++ ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long entityLoadId; ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ this.ticketLock.lock(); ++ try { ++ entityLoadId = Long.valueOf(this.entityLoadCounter++); ++ this.addTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ this.removeTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ return ret; ++ } ++ ++ if (current.isEntityChunkNBTLoaded()) { ++ isCompleted.setPlain(true); ++ } else { ++ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> { ++ if (!transientChunk) { ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ } ++ }); ++ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); ++ ++ if (entityLoad != null && !transientChunk) { ++ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ } ++ ++ if (!transientChunk) { ++ // Note: no need to busy wait on the chunk queue, entity load will complete off-main ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ ++ // now that the entity data is loaded, we can load it into the world ++ ++ ret = current.loadInEntityChunk(transientChunk); ++ ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ this.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ TicketType.ENTITY_LOAD, MAX_TICKET_LEVEL, entityLoadId ++ ); ++ ++ return ret; ++ } ++ ++ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { ++ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); ++ if (holder != null) { ++ final PoiChunk ret = holder.getPoiChunk(); ++ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; ++ } ++ return null; ++ } ++ ++ private long poiLoadCounter; ++ ++ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); ++ PoiChunk ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getPoiChunk()) != null) { ++ if (!ret.isLoaded()) { ++ ret.load(); ++ } ++ return ret; ++ } ++ ++ final AtomicReference completed = new AtomicReference<>(); ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long poiLoadId; ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ this.ticketLock.lock(); ++ try { ++ poiLoadId = Long.valueOf(this.poiLoadCounter++); ++ this.addTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if (current.isPoiChunkLoaded()) { ++ this.removeTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ return current.getPoiChunk(); ++ } ++ ++ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> { ++ completed.setPlain(result.left()); ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ }); ++ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ } ++ ++ // Note: no need to busy wait on the chunk queue, poi load will complete off-main ++ ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ ++ ret = completed.getPlain(); ++ ++ ret.load(); ++ ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ this.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ TicketType.POI_LOAD, MAX_TICKET_LEVEL, poiLoadId ++ ); ++ ++ return ret; ++ } ++ ++ void addChangedStatuses(final List changedFullStatus) { ++ if (changedFullStatus.isEmpty()) { ++ return; ++ } ++ if (!TickThread.isTickThread()) { ++ this.taskScheduler.scheduleChunkTask(() -> { ++ final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ ++ ChunkHolderManager.this.processPendingFullUpdate(); ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } else { ++ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ } ++ } ++ ++ final ReferenceLinkedOpenHashSet unloadQueue = new ReferenceLinkedOpenHashSet<>(); ++ ++ private void removeChunkHolder(final NewChunkHolder holder) { ++ holder.killed = true; ++ holder.vanillaChunkHolder.onChunkRemove(); ++ this.autoSaveQueue.remove(holder); ++ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); ++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); ++ } ++ ++ // note: never call while inside the chunk system, this will absolutely break everything ++ public void processUnloads() { ++ TickThread.ensureTickThread("Cannot unload chunks off-main"); ++ ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot unload chunks recursively"); ++ } ++ if (this.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot hold ticket update lock while calling processUnloads"); ++ } ++ if (this.taskScheduler.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot hold scheduling lock while calling processUnloads"); ++ } ++ ++ final List unloadQueue; ++ final List scheduleList = new ArrayList<>(); ++ this.ticketLock.lock(); ++ try { ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ if (this.unloadQueue.isEmpty()) { ++ return; ++ } ++ // in order to ensure all chunks in the unload queue do not have a pending ticket level update, ++ // process them now ++ this.processTicketUpdates(false, false, scheduleList); ++ unloadQueue = new ArrayList<>((int)(this.unloadQueue.size() * 0.05) + 1); ++ ++ final int unloadCount = Math.max(50, (int)(this.unloadQueue.size() * 0.05)); ++ for (int i = 0; i < unloadCount && !this.unloadQueue.isEmpty(); ++i) { ++ final NewChunkHolder chunkHolder = this.unloadQueue.removeFirst(); ++ if (chunkHolder.isSafeToUnload() != null) { ++ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); ++ continue; ++ } ++ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); ++ if (state == null) { ++ // can unload immediately ++ this.removeChunkHolder(chunkHolder); ++ continue; ++ } ++ unloadQueue.add(state); ++ } ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ // schedule tasks, we can't let processTicketUpdates do this because we call it holding the schedule lock ++ for (int i = 0, len = scheduleList.size(); i < len; ++i) { ++ scheduleList.get(i).schedule(); ++ } ++ ++ final List toRemove = new ArrayList<>(unloadQueue.size()); ++ ++ final Boolean before = this.blockTicketUpdates(); ++ try { ++ for (int i = 0, len = unloadQueue.size(); i < len; ++i) { ++ final NewChunkHolder.UnloadState state = unloadQueue.get(i); ++ final NewChunkHolder holder = state.holder(); ++ ++ holder.unloadStage2(state); ++ toRemove.add(holder); ++ } ++ } finally { ++ this.unblockTicketUpdates(before); ++ } ++ ++ this.ticketLock.lock(); ++ try { ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ for (int i = 0, len = toRemove.size(); i < len; ++i) { ++ final NewChunkHolder holder = toRemove.get(i); ++ ++ if (holder.unloadStage3()) { ++ this.removeChunkHolder(holder); ++ } else { ++ // add cooldown so the next unload check is not immediately next tick ++ this.addTicketAtLevel(TicketType.UNLOAD_COOLDOWN, holder.chunkX, holder.chunkZ, MAX_TICKET_LEVEL, Unit.INSTANCE); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ ++ private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { ++ return Boolean.FALSE; ++ }); ++ ++ public Boolean blockTicketUpdates() { ++ final Boolean ret = BLOCK_TICKET_UPDATES.get(); ++ BLOCK_TICKET_UPDATES.set(Boolean.TRUE); ++ return ret; ++ } ++ ++ public void unblockTicketUpdates(final Boolean before) { ++ BLOCK_TICKET_UPDATES.set(before); ++ } ++ ++ public boolean processTicketUpdates() { ++ return this.processTicketUpdates(true, true, null); ++ } ++ ++ private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); ++ ++ static List getCurrentTicketUpdateScheduling() { ++ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ } ++ ++ private boolean processTicketUpdates(final boolean checkLocks, final boolean processFullUpdates, List scheduledTasks) { ++ TickThread.ensureTickThread("Cannot process ticket levels off-main"); ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); ++ } ++ if (checkLocks && this.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Illegal recursive processTicketUpdates!"); ++ } ++ if (checkLocks && this.taskScheduler.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot update ticket levels from a scheduler context!"); ++ } ++ ++ List changedFullStatus = null; ++ ++ final boolean isTickThread = TickThread.isTickThread(); ++ ++ boolean ret = false; ++ final boolean canProcessFullUpdates = processFullUpdates & isTickThread; ++ final boolean canProcessScheduling = scheduledTasks == null; ++ ++ this.ticketLock.lock(); ++ try { ++ final boolean levelsUpdated = this.ticketLevelPropagator.propagateUpdates(); ++ if (levelsUpdated) { ++ // Unlike CB, ticket level updates cannot happen recursively. Thank god. ++ if (!this.ticketLevelUpdates.isEmpty()) { ++ ret = true; ++ ++ // first the necessary chunkholders must be created, so just update the ticket levels ++ for (final Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ final Long2IntMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final int newLevel = entry.getIntValue(); ++ ++ NewChunkHolder current = this.chunkHolders.get(key); ++ if (current == null && newLevel > MAX_TICKET_LEVEL) { ++ // not loaded and it shouldn't be loaded! ++ iterator.remove(); ++ continue; ++ } ++ ++ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); ++ if (currentLevel == newLevel) { ++ // nothing to do ++ iterator.remove(); ++ continue; ++ } ++ ++ if (current == null) { ++ // must create ++ current = this.createChunkHolder(key); ++ this.chunkHolders.put(key, current); ++ current.updateTicketLevel(newLevel); ++ } else { ++ current.updateTicketLevel(newLevel); ++ } ++ } ++ ++ if (scheduledTasks == null) { ++ scheduledTasks = new ArrayList<>(); ++ } ++ changedFullStatus = new ArrayList<>(); ++ ++ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time ++ final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); ++ try { ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ for (final Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ final Long2IntMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final NewChunkHolder current = this.chunkHolders.get(key); ++ ++ if (current == null) { ++ throw new IllegalStateException("Expected chunk holder to be created"); ++ } ++ ++ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); ++ } ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); ++ } ++ ++ this.ticketLevelUpdates.clear(); ++ } ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ ++ if (changedFullStatus != null) { ++ this.addChangedStatuses(changedFullStatus); ++ } ++ ++ if (canProcessScheduling && scheduledTasks != null) { ++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { ++ scheduledTasks.get(i).schedule(); ++ } ++ } ++ ++ if (canProcessFullUpdates) { ++ ret |= this.processPendingFullUpdate(); ++ } ++ ++ return ret; ++ } ++ ++ // only call on tick thread ++ protected final boolean processPendingFullUpdate() { ++ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ ++ boolean ret = false; ++ ++ List changedFullStatus = new ArrayList<>(); ++ ++ NewChunkHolder holder; ++ while ((holder = pendingFullLoadUpdate.poll()) != null) { ++ ret |= holder.handleFullStatusChange(changedFullStatus); ++ ++ if (!changedFullStatus.isEmpty()) { ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ changedFullStatus.clear(); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJsonForWatchdog() { ++ // try and detect any potential deadlock that would require us to read unlocked ++ try { ++ if (this.ticketLock.tryLock(10, TimeUnit.SECONDS)) { ++ try { ++ if (this.taskScheduler.schedulingLock.tryLock(10, TimeUnit.SECONDS)) { ++ try { ++ return this.getDebugJsonNoLock(); ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ } ++ } catch (final InterruptedException ignore) {} ++ ++ LOGGER.error("Failed to acquire ticket and scheduling lock before timeout for world " + this.world.getWorld().getName()); ++ ++ // because we read without locks, it may throw exceptions for fastutil maps ++ // so just try until it works... ++ Throwable lastException = null; ++ for (int count = 0;count < 1000;++count) { ++ try { ++ return this.getDebugJsonNoLock(); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ lastException = thr; ++ Thread.yield(); ++ LockSupport.parkNanos(10_000L); ++ } ++ } ++ ++ // failed, return ++ LOGGER.error("Failed to retrieve debug json for watchdog thread without locking", lastException); ++ return null; ++ } ++ ++ private JsonObject getDebugJsonNoLock() { ++ final JsonObject ret = new JsonObject(); ++ ret.addProperty("current_tick", Long.valueOf(this.currentTick)); ++ ++ final JsonArray unloadQueue = new JsonArray(); ++ ret.add("unload_queue", unloadQueue); ++ for (final NewChunkHolder holder : this.unloadQueue) { ++ final JsonObject coordinate = new JsonObject(); ++ unloadQueue.add(coordinate); ++ ++ coordinate.addProperty("chunkX", Integer.valueOf(holder.chunkX)); ++ coordinate.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); ++ } ++ ++ final JsonArray holders = new JsonArray(); ++ ret.add("chunkholders", holders); ++ ++ for (final NewChunkHolder holder : this.getChunkHolders()) { ++ holders.add(holder.getDebugJson()); ++ } ++ ++ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); ++ ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); ++ ++ for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { ++ final long tick = tickEntry.getLongKey(); ++ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); ++ ++ final JsonObject tickJson = new JsonObject(); ++ removeTickToChunkExpireTicketCount.add(tickJson); ++ ++ tickJson.addProperty("tick", Long.valueOf(tick)); ++ ++ final JsonArray tickEntries = new JsonArray(); ++ tickJson.add("entries", tickEntries); ++ ++ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { ++ final long coordinate = entry.getLongKey(); ++ final int count = entry.getIntValue(); ++ ++ final JsonObject entryJson = new JsonObject(); ++ tickEntries.add(entryJson); ++ ++ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ entryJson.addProperty("count", Integer.valueOf(count)); ++ } ++ } ++ ++ final JsonArray allTicketsJson = new JsonArray(); ++ ret.add("tickets", allTicketsJson); ++ ++ for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { ++ final long coordinate = coordinateTickets.getLongKey(); ++ final SortedArraySet> tickets = coordinateTickets.getValue(); ++ ++ final JsonObject coordinateJson = new JsonObject(); ++ allTicketsJson.add(coordinateJson); ++ ++ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ ++ final JsonArray ticketsSerialized = new JsonArray(); ++ coordinateJson.add("tickets", ticketsSerialized); ++ ++ for (final Ticket ticket : tickets) { ++ final JsonObject ticketSerialized = new JsonObject(); ++ ticketsSerialized.add(ticketSerialized); ++ ++ ticketSerialized.addProperty("type", ticket.getType().toString()); ++ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); ++ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); ++ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJson() { ++ final List scheduleList = new ArrayList<>(); ++ try { ++ final JsonObject ret; ++ this.ticketLock.lock(); ++ try { ++ this.taskScheduler.schedulingLock.lock(); ++ try { ++ this.processTicketUpdates(false, false, scheduleList); ++ ret = this.getDebugJsonNoLock(); ++ } finally { ++ this.taskScheduler.schedulingLock.unlock(); ++ } ++ } finally { ++ this.ticketLock.unlock(); ++ } ++ return ret; ++ } finally { ++ // schedule tasks, we can't let processTicketUpdates do this because we call it holding the schedule lock ++ for (int i = 0, len = scheduleList.size(); i < len; ++i) { ++ scheduleList.get(i).schedule(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.starlight.common.light.StarLightEngine; ++import ca.spottedleaf.starlight.common.light.StarLightInterface; ++import io.papermc.paper.chunk.system.light.LightQueue; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++import java.util.function.BooleanSupplier; ++ ++public final class ChunkLightTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ protected final ChunkAccess fromChunk; ++ ++ private final LightTaskPriorityHolder priorityHolder; ++ ++ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.priorityHolder = new LightTaskPriorityHolder(priority, this); ++ this.fromChunk = chunk; ++ } ++ ++ @Override ++ public boolean isScheduled() { ++ return this.priorityHolder.isScheduled(); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.LIGHT; ++ } ++ ++ @Override ++ public void schedule() { ++ this.priorityHolder.schedule(); ++ } ++ ++ @Override ++ public void cancel() { ++ this.priorityHolder.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.priorityHolder.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ private static final class LightTaskPriorityHolder extends PriorityHolder { ++ ++ protected final ChunkLightTask task; ++ ++ protected LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { ++ super(priority); ++ this.task = task; ++ } ++ ++ @Override ++ protected void cancelScheduled() { ++ final ChunkLightTask task = this.task; ++ task.complete(null, null); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.Priority getScheduledPriority() { ++ final ChunkLightTask task = this.task; ++ return task.world.getChunkSource().getLightEngine().theLightEngine.lightQueue.getPriority(task.chunkX, task.chunkZ); ++ } ++ ++ @Override ++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); ++ } ++ } ++ ++ private static final class LightTask implements BooleanSupplier { ++ ++ protected final StarLightInterface lightEngine; ++ protected final ChunkLightTask task; ++ ++ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { ++ this.lightEngine = lightEngine; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean getAsBoolean() { ++ final ChunkLightTask task = this.task; ++ // executed on light thread ++ if (!task.priorityHolder.markExecuting()) { ++ // cancelled ++ return false; ++ } ++ ++ try { ++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); ++ ++ if (task.fromChunk.isLightCorrect() && task.fromChunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); ++ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); ++ } else { ++ task.fromChunk.setLightCorrect(false); ++ this.lightEngine.lightChunk(task.fromChunk, emptySections); ++ task.fromChunk.setLightCorrect(true); ++ } ++ // we need to advance status ++ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getStatus() == ChunkStatus.LIGHT.getParent()) { ++ chunk.setStatus(ChunkStatus.LIGHT); ++ } ++ } catch (final Throwable thr) { ++ if (!(thr instanceof ThreadDeath)) { ++ LOGGER.fatal("Failed to light chunk " + task.fromChunk.getPos().toString() + " in world '" + this.lightEngine.getWorld().getWorld().getName() + "'", thr); ++ } ++ ++ task.complete(null, thr); ++ ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ ++ return true; ++ } ++ ++ task.complete(task.fromChunk, null); ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import net.minecraft.SharedConstants; ++import net.minecraft.core.Registry; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.UpgradeData; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.levelgen.blending.BlendingData; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Consumer; ++ ++public final class ChunkLoadTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private final NewChunkHolder chunkHolder; ++ private final ChunkDataLoadTask loadTask; ++ ++ private boolean cancelled; ++ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ ++ protected ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); ++ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { ++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); ++ }); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.EMPTY; ++ } ++ ++ private boolean scheduled; ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ ++ final AtomicInteger count = new AtomicInteger(); ++ final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { ++ if (count.decrementAndGet() == 0) { ++ ChunkLoadTask.this.loadTask.schedule(false); ++ } ++ }; ++ ++ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because ++ // they must schedule a task to off main or to on main to complete ++ this.scheduler.schedulingLock.lock(); ++ try { ++ if (this.scheduled) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ this.scheduled = true; ++ if (this.cancelled) { ++ return; ++ } ++ if (!this.chunkHolder.isEntityChunkNBTLoaded()) { ++ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); ++ count.setPlain(count.getPlain() + 1); ++ } else { ++ entityLoadTask = null; ++ } ++ ++ if (!this.chunkHolder.isPoiChunkLoaded()) { ++ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); ++ count.setPlain(count.getPlain() + 1); ++ } else { ++ poiLoadTask = null; ++ } ++ ++ this.entityLoadTask = entityLoadTask; ++ this.poiLoadTask = poiLoadTask; ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ ++ if (entityLoadTask != null) { ++ entityLoadTask.schedule(); ++ } ++ ++ if (poiLoadTask != null) { ++ poiLoadTask.schedule(); ++ } ++ ++ if (entityLoadTask == null && poiLoadTask == null) { ++ // no need to wait on those, we can schedule now ++ this.loadTask.schedule(false); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ // must be before load task access, so we can synchronise with the writes to the fields ++ this.scheduler.schedulingLock.lock(); ++ try { ++ this.cancelled = true; ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ ++ /* ++ Note: The entityLoadTask/poiLoadTask do not complete when cancelled, ++ but this is fine because if they are successfully cancelled then ++ we will successfully cancel the load task, which will complete when cancelled ++ */ ++ ++ if (this.entityLoadTask != null) { ++ this.entityLoadTask.cancel(); ++ } ++ if (this.poiLoadTask != null) { ++ this.poiLoadTask.cancel(); ++ } ++ this.loadTask.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.loadTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.lowerPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.lowerPriority(priority); ++ } ++ ++ this.loadTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.setPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.setPriority(priority); ++ } ++ ++ this.loadTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.raisePriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(priority); ++ } ++ ++ this.loadTask.raisePriority(priority); ++ } ++ ++ protected static abstract class CallbackDataLoadTask extends GenericDataLoadTask { ++ ++ private TaskResult result; ++ private final MultiThreadedQueue>> waiters = new MultiThreadedQueue<>(); ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); ++ ++ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, type, priority); ++ } ++ ++ public void addCallback(final Consumer> consumer) { ++ if (!this.waiters.add(consumer)) { ++ try { ++ consumer.accept(this.result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ } ++ } ++ } ++ ++ @Override ++ protected void onComplete(final TaskResult result) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.result = result; ++ Consumer> consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ } ++ } ++ } ++ ++ public final class ChunkDataLoadTask extends CallbackDataLoadTask { ++ protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return true; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final ChunkSerializer.InProgressChunkHolder data, final Throwable throwable) { ++ if (data != null) { ++ return null; ++ } ++ ++ final PoiChunk poiChunk = ChunkLoadTask.this.chunkHolder.getPoiChunk(); ++ if (poiChunk == null) { ++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); ++ } else if (!poiChunk.isLoaded()) { ++ // need to call poiChunk.load() on main ++ return null; ++ } ++ ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null) { ++ return new TaskResult<>(null, null); ++ } ++ ++ // need to convert data, and then deserialize it ++ ++ try { ++ final ChunkPos chunkPos = new ChunkPos(this.chunkX, this.chunkZ); ++ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; ++ // run converters ++ // note: upgradeChunkTag copies the data already ++ final CompoundTag converted = chunkMap.upgradeChunkTag( ++ this.world.getTypeKey(), chunkMap.overworldDataStorage, data, chunkMap.generator.getTypeNameForDataFixer(), ++ chunkPos, this.world ++ ); ++ // deserialize ++ final ChunkSerializer.InProgressChunkHolder chunkHolder = ChunkSerializer.loadChunk( ++ this.world, chunkMap.getPoiManager(), chunkPos, converted, true ++ ); ++ ++ return new TaskResult<>(chunkHolder, null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(null, thr2); ++ } ++ } ++ ++ private ProtoChunk getEmptyChunk() { ++ return new ProtoChunk( ++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, ++ this.world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), (BlendingData)null ++ ); ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final ChunkSerializer.InProgressChunkHolder data, final Throwable throwable) { ++ final PoiChunk poiChunk = ChunkLoadTask.this.chunkHolder.getPoiChunk(); ++ if (poiChunk == null) { ++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); ++ } else { ++ poiChunk.load(); ++ } ++ ++ if (data == null || data.protoChunk == null) { ++ // throwable could be non-null, but the off-main task will print its exceptions - so we don't need to care, ++ // it's handled already ++ ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ // have tasks to run (at this point, it's just the POI consistency checking) ++ try { ++ if (data.tasks != null) { ++ for (int i = 0, len = data.tasks.size(); i < len; ++i) { ++ data.tasks.poll().run(); ++ } ++ } ++ ++ return new TaskResult<>(data.protoChunk, null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse main tasks for task " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ } ++ } ++ ++ public static final class PoiDataLoadTask extends CallbackDataLoadTask { ++ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ try { ++ data = data.copy(); // coming from the I/O thread, so we need to copy ++ // run converters ++ final int dataVersion = !data.contains(SharedConstants.DATA_VERSION_TAG, 99) ? 1945 : data.getInt(SharedConstants.DATA_VERSION_TAG); ++ final CompoundTag converted = MCDataConverter.convertTag( ++ MCTypeRegistry.POI_CHUNK, data, dataVersion, SharedConstants.getCurrentVersion().getWorldVersion() ++ ); ++ ++ // now we need to parse it ++ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ ++ public static final class EntityDataLoadTask extends CallbackDataLoadTask { ++ ++ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(null, null); ++ } ++ ++ try { ++ // note: data comes from the I/O thread, so we need to copy it ++ return new TaskResult<>(EntityStorage.upgradeChunkTag(data.copy()), null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); ++ return new TaskResult<>(null, thr2); ++ } ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.function.BiConsumer; ++ ++public abstract class ChunkProgressionTask { ++ ++ private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); ++ private ChunkAccess completedChunk; ++ private Throwable completedThrowable; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); ++ ++ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ } ++ ++ // Used only for debug json ++ public abstract boolean isScheduled(); ++ ++ // Note: It is the responsibility of the task to set the chunk's status once it has completed ++ public abstract ChunkStatus getTargetStatus(); ++ ++ /* Only executed once */ ++ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ ++ public abstract void schedule(); ++ ++ /* May be called multiple times */ ++ public abstract void cancel(); ++ ++ public abstract PrioritisedExecutor.Priority getPriority(); ++ ++ /* Schedule lock is always held for the priority update calls */ ++ ++ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void setPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void raisePriority(final PrioritisedExecutor.Priority priority); ++ ++ public final void onComplete(final BiConsumer onComplete) { ++ if (!this.waiters.add(onComplete)) { ++ try { ++ onComplete.accept(this.completedChunk, this.completedThrowable); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ } ++ } ++ } ++ ++ protected final void complete(final ChunkAccess chunk, final Throwable throwable) { ++ try { ++ this.complete0(chunk, throwable); ++ } catch (final Throwable thr2) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2); ++ if (thr2 instanceof ThreadDeath) { ++ throw (ThreadDeath)thr2; ++ } ++ } ++ } ++ ++ private void complete0(final ChunkAccess chunk, final Throwable throwable) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.completedChunk = chunk; ++ this.completedThrowable = throwable; ++ ++ BiConsumer consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ consumer.accept(chunk, throwable); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + this.world.getWorld().getName() + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import net.minecraft.CrashReport; ++import net.minecraft.CrashReportCategory; ++import net.minecraft.ReportedException; ++import net.minecraft.server.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunk; ++import org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.io.File; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.ReentrantLock; ++import java.util.function.BooleanSupplier; ++import java.util.function.Consumer; ++ ++public final class ChunkTaskScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ static int newChunkSystemIOThreads; ++ static int newChunkSystemWorkerThreads; ++ static int newChunkSystemGenParallelism; ++ static int newChunkSystemLoadParallelism; ++ ++ public static ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool workerThreads; ++ ++ private static boolean initialised = false; ++ ++ public static void init(final GlobalConfiguration.ChunkSystem config) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ newChunkSystemIOThreads = config.ioThreads; ++ newChunkSystemWorkerThreads = config.workerThreads; ++ if (newChunkSystemIOThreads < 0) { ++ newChunkSystemIOThreads = 1; ++ } else { ++ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); ++ } ++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (defaultWorkerThreads <= 4) { ++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; ++ } else { ++ defaultWorkerThreads = defaultWorkerThreads / 2; ++ } ++ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); ++ ++ if (newChunkSystemWorkerThreads < 0) { ++ newChunkSystemWorkerThreads = defaultWorkerThreads; ++ } else { ++ newChunkSystemWorkerThreads = Math.max(1, newChunkSystemWorkerThreads); ++ } ++ ++ String newChunkSystemGenParallelism = config.genParallelism; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { ++ newChunkSystemGenParallelism = "true"; ++ } ++ boolean useParallelGen; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { ++ useParallelGen = true; ++ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { ++ useParallelGen = false; ++ } else { ++ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); ++ } ++ ++ ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? newChunkSystemWorkerThreads : 1; ++ ChunkTaskScheduler.newChunkSystemLoadParallelism = newChunkSystemWorkerThreads; ++ ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.init(newChunkSystemIOThreads); ++ workerThreads = new ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool( ++ "Paper Chunk System Worker Pool", newChunkSystemWorkerThreads, ++ (final Thread thread, final Integer id) -> { ++ thread.setPriority(Thread.NORM_PRIORITY - 2); ++ thread.setName("Tuinity Chunk System Worker #" + id.intValue()); ++ thread.setUncaughtExceptionHandler(io.papermc.paper.chunk.system.scheduling.NewChunkHolder.CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER); ++ }, (long)(20.0e6)); // 20ms ++ ++ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + newChunkSystemWorkerThreads + " worker threads, and gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenParallelism + " threads"); ++ } ++ ++ public final ServerLevel world; ++ public final PrioritisedThreadPool workers; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor lightExecutor; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor genExecutor; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; ++ ++ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); ++ ++ final ReentrantLock schedulingLock = new ReentrantLock(); ++ public final ChunkHolderManager chunkHolderManager; ++ ++ static { ++ ChunkStatus.EMPTY.writeRadius = 0; ++ ChunkStatus.STRUCTURE_STARTS.writeRadius = 0; ++ ChunkStatus.STRUCTURE_REFERENCES.writeRadius = 0; ++ ChunkStatus.BIOMES.writeRadius = 0; ++ ChunkStatus.NOISE.writeRadius = 0; ++ ChunkStatus.SURFACE.writeRadius = 0; ++ ChunkStatus.CARVERS.writeRadius = 0; ++ ChunkStatus.LIQUID_CARVERS.writeRadius = 0; ++ ChunkStatus.FEATURES.writeRadius = 1; ++ ChunkStatus.LIGHT.writeRadius = 1; ++ ChunkStatus.SPAWN.writeRadius = 0; ++ ChunkStatus.HEIGHTMAPS.writeRadius = 0; ++ ChunkStatus.FULL.writeRadius = 0; ++ ++ /* ++ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as ++ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, ++ for a status to be parallel safe it must not read the block data from its neighbours. ++ */ ++ final List parallelCapableStatus = Arrays.asList( ++ // No-op executor. ++ ChunkStatus.EMPTY, ++ ++ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. ++ // Does not touch neighbour chunks. ++ // TODO On another note, what the fuck is StructureFeatureManager.StructureCheck and why is it used? it's leaking ++ ChunkStatus.STRUCTURE_STARTS, ++ ++ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts ++ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours ++ // will not change, even if executed in parallel. ++ ChunkStatus.STRUCTURE_REFERENCES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.BIOMES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.NOISE, ++ ++ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is ++ // completely thread-safe. ++ ChunkStatus.SURFACE, ++ ++ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. ++ ChunkStatus.CARVERS, ++ ++ // No-op executor. Was replaced in 1.18 with carvers, I think. ++ ChunkStatus.LIQUID_CARVERS, ++ ++ // FEATURES is not parallel safe. It writes to neighbours. ++ ++ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. ++ ++ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. ++ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - ++ // see the task margin. ++ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever ++ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not ++ // safe to execute this in parallel. ++ // SPAWN ++ ++ // No-op executor. ++ ChunkStatus.HEIGHTMAPS ++ ++ // FULL is executed on main. ++ ); ++ ++ for (final ChunkStatus status : parallelCapableStatus) { ++ status.isParallelCapable = true; ++ } ++ } ++ ++ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { ++ this.world = world; ++ this.workers = workers; ++ ++ final String worldName = world.getWorld().getName(); ++ this.genExecutor = workers.createExecutor("Chunk single-threaded generation executor for world '" + worldName + "'", 1); ++ // same as genExecutor, as there are race conditions between updating blocks in FEATURE status while lighting chunks ++ this.lightExecutor = this.genExecutor; ++ this.parallelGenExecutor = newChunkSystemGenParallelism <= 1 ? this.genExecutor ++ : workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", newChunkSystemGenParallelism); ++ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", newChunkSystemLoadParallelism); ++ this.chunkHolderManager = new ChunkHolderManager(world, this); ++ } ++ ++ private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); ++ ++ public static Object stringIfNull(final Object obj) { ++ return obj == null ? "null" : obj; ++ } ++ ++ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map objectsOfInterest, final Throwable thr) { ++ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); ++ ++ if (this.failedChunkSystem.getAndSet(true)) { ++ return; ++ } ++ ++ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); ++ ++ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); ++ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); ++ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); ++ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); ++ ++ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); ++ for (final Map.Entry entry : objectsOfInterest.entrySet()) { ++ if (entry.getValue() instanceof Throwable thrObject) { ++ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); ++ } else { ++ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); ++ } ++ } ++ ++ final Runnable crash = () -> { ++ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); ++ }; ++ ++ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions ++ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); ++ // so, make the main thread pick it up ++ MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); ++ } ++ ++ public boolean executeMainThreadTask() { ++ TickThread.ensureTickThread("Cannot execute main thread task off-main"); ++ return this.mainThreadExecutor.executeTask(); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.raisePriority(x, z, priority); ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.setPriority(x, z, priority); ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.lowerPriority(x, z, priority); ++ } ++ ++ private final AtomicLong chunkLoadCounter = new AtomicLong(); ++ ++ public void scheduleTickingState(final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus toStatus, ++ final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final Consumer onComplete) { ++ if (!TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ if (this.chunkHolderManager.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); ++ } ++ ++ final int minLevel = 33 - (toStatus.ordinal() - 1); ++ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer loadCallback = (final LevelChunk chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), ++ TicketType.CHUNK_LOAD, minLevel, chunkReference ++ ); ++ } ++ } ++ }; ++ ++ final boolean scheduled; ++ final LevelChunk chunk; ++ this.chunkHolderManager.ticketLock.lock(); ++ try { ++ this.schedulingLock.lock(); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final ChunkHolder.FullChunkStatus currStatus = chunkHolder.getChunkStatus(); ++ if (currStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = (LevelChunk)chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ ++ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ final NewChunkHolder neighbour = ++ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ if (neighbour != null) { ++ neighbour.raisePriority(priority); ++ } ++ } ++ } ++ ++ // ticket level should schedule for us ++ chunkHolder.addFullStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLock.unlock(); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLock.unlock(); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk full status callback", thr); ++ } ++ } ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { ++ if (gen) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ return; ++ } ++ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { ++ if (chunk == null) { ++ onComplete.accept(null); ++ } else { ++ if (chunk.getStatus().isOrAfter(toStatus)) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } else { ++ onComplete.accept(null); ++ } ++ } ++ }); ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { ++ if (!TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ if (this.chunkHolderManager.ticketLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == ChunkStatus.FULL) { ++ this.scheduleTickingState(chunkX, chunkZ, ChunkHolder.FullChunkStatus.BORDER, addTicket, priority, (Consumer)onComplete); ++ return; ++ } ++ ++ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); ++ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer loadCallback = (final ChunkAccess chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), ++ TicketType.CHUNK_LOAD, minLevel, chunkReference ++ ); ++ } ++ } ++ }; ++ ++ final List tasks = new ArrayList<>(); ++ ++ final boolean scheduled; ++ final ChunkAccess chunk; ++ this.chunkHolderManager.ticketLock.lock(); ++ try { ++ this.schedulingLock.lock(); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ chunkHolder.addStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLock.unlock(); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLock.unlock(); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ } ++ ++ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, ++ final NewChunkHolder chunkHolder, final List neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { ++ if (toStatus == ChunkStatus.EMPTY) { ++ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); ++ } ++ if (toStatus == ChunkStatus.LIGHT) { ++ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); ++ } ++ if (toStatus == ChunkStatus.FULL) { ++ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); ++ } ++ ++ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); ++ } ++ ++ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, ++ final List allTasks) { ++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority()); ++ } ++ ++ // rets new task scheduled for the _specified_ chunk ++ // note: this must hold the scheduling lock ++ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done ++ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! ++ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, ++ final NewChunkHolder chunkHolder, final List allTasks, ++ final PrioritisedExecutor.Priority minPriority) { ++ if (!this.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Not holding scheduling lock"); ++ } ++ ++ if (chunkHolder.hasGenerationTask()) { ++ chunkHolder.upgradeGenTarget(targetStatus); ++ return null; ++ } ++ ++ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(minPriority, chunkHolder.getEffectivePriority()); ++ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); ++ final ChunkAccess chunk = chunkHolder.getCurrentChunk(); ++ ++ if (currentGenStatus == null) { ++ // not yet loaded ++ final ChunkProgressionTask task = this.createTask( ++ chunkX, chunkZ, chunk, chunkHolder, Collections.emptyList(), ChunkStatus.EMPTY, requestedPriority ++ ); ++ ++ allTasks.add(task); ++ ++ final List chunkHolderNeighbours = new ArrayList<>(1); ++ chunkHolderNeighbours.add(chunkHolder); ++ ++ chunkHolder.setGenerationTarget(targetStatus); ++ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ if (currentGenStatus.isOrAfter(targetStatus)) { ++ // nothing to do ++ return null; ++ } ++ ++ // we know for sure now that we want to schedule _something_, so set the target ++ chunkHolder.setGenerationTarget(targetStatus); ++ ++ final ChunkStatus chunkRealStatus = chunk.getStatus(); ++ final ChunkStatus toStatus = currentGenStatus.getNextStatus(); ++ ++ // if this chunk has already generated up to or past the specified status, then we don't ++ // need the neighbours AT ALL. ++ final int neighbourReadRadius = chunkRealStatus.isOrAfter(toStatus) ? toStatus.loadRange : toStatus.getRange(); ++ ++ boolean unGeneratedNeighbours = false; ++ ++ // copied from MCUtil.getSpiralOutChunks ++ for (int r = 1; r <= neighbourReadRadius; r++) { ++ int x = -r; ++ int z = r; ++ ++ // Iterates the edge of half of the box; then negates for other half. ++ while (x <= r && z > -r) { ++ final int radius = Math.max(Math.abs(x), Math.abs(z)); ++ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(toStatus, radius); ++ ++ unGeneratedNeighbours |= this.checkNeighbour( ++ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority ++ ); ++ unGeneratedNeighbours |= this.checkNeighbour( ++ chunkX - x, chunkZ - z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority ++ ); ++ ++ if (x < r) { ++ x++; ++ } else { ++ z--; ++ } ++ } ++ } ++ ++ if (unGeneratedNeighbours) { ++ // can't schedule, but neighbour completion will schedule for us when they're ALL done ++ ++ // propagate our priority to neighbours ++ chunkHolder.recalculateNeighbourPriorities(); ++ return null; ++ } ++ ++ // need to gather neighbours ++ ++ final List neighbours; ++ final List chunkHolderNeighbours; ++ if (neighbourReadRadius <= 0) { ++ neighbours = new ArrayList<>(1); ++ chunkHolderNeighbours = new ArrayList<>(1); ++ neighbours.add(chunk); ++ chunkHolderNeighbours.add(chunkHolder); ++ } else { ++ // the iteration order is _very_ important, as all generation statuses expect a certain order such that: ++ // chunkAtRelative = neighbours.get(relX + relZ * (2 * radius + 1)) ++ neighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ for (int dz = -neighbourReadRadius; dz <= neighbourReadRadius; ++dz) { ++ for (int dx = -neighbourReadRadius; dx <= neighbourReadRadius; ++dx) { ++ final NewChunkHolder holder = (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ neighbours.add(holder.getChunkForNeighbourAccess()); ++ chunkHolderNeighbours.add(holder); ++ } ++ } ++ } ++ ++ final ChunkProgressionTask task = this.createTask(chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, chunkHolder.getEffectivePriority()); ++ allTasks.add(task); ++ ++ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ // rets true if the neighbour is not at the required status, false otherwise ++ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, ++ final List tasks, final PrioritisedExecutor.Priority minPriority) { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ ++ if (chunkHolder == null) { ++ throw new IllegalStateException("Missing chunkholder when required"); ++ } ++ ++ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); ++ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { ++ return false; ++ } ++ ++ if (chunkHolder.hasFailedGeneration()) { ++ return true; ++ } ++ ++ center.addGenerationBlockingNeighbour(chunkHolder); ++ chunkHolder.addWaitingNeighbour(center, requiredStatus); ++ ++ if (chunkHolder.upgradeGenTarget(requiredStatus)) { ++ return true; ++ } ++ ++ // not at status required, so we need to schedule its generation ++ this.schedule( ++ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority ++ ); ++ ++ return true; ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { ++ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.createTask(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.mainThreadExecutor.queueRunnable(run); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public void executeTasksUntil(final BooleanSupplier exit) { ++ if (Bukkit.isPrimaryThread()) { ++ this.mainThreadExecutor.executeConditionally(exit); ++ } else { ++ long counter = 1L; ++ while (!exit.getAsBoolean()) { ++ counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms ++ } ++ } ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ this.lightExecutor.halt(); ++ this.genExecutor.halt(); ++ this.parallelGenExecutor.halt(); ++ this.loadExecutor.halt(); ++ final long time = System.nanoTime(); ++ if (sync) { ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ if ( ++ !this.lightExecutor.isActive() && ++ !this.genExecutor.isActive() && ++ !this.parallelGenExecutor.isActive() && ++ !this.loadExecutor.isActive() ++ ) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack ++ ++ public 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(); ++ } ++ } ++ ++ public static ChunkInfo[] getChunkInfos() { ++ synchronized (WAITING_CHUNKS) { ++ return WAITING_CHUNKS.toArray(new ChunkInfo[0]); ++ } ++ } ++ ++ public static void dumpAllChunkLoadInfo(final boolean longPrint) { ++ final ChunkInfo[] chunkInfos = getChunkInfos(); ++ if (chunkInfos.length > 0) { ++ LOGGER.error("Chunk wait task info below: "); ++ for (final ChunkInfo chunkInfo : chunkInfos) { ++ final NewChunkHolder holder = chunkInfo.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); ++ LOGGER.error("Chunk wait: " + chunkInfo); ++ LOGGER.error("Chunk holder: " + holder); ++ } ++ ++ if (longPrint) { ++ final File file = new File(new File(new File("."), "debug"), "chunks-watchdog.txt"); ++ LOGGER.error("Writing chunk information dump to " + file); ++ try { ++ MCUtil.dumpChunks(file, true); ++ LOGGER.error("Successfully written chunk information!"); ++ } catch (final Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.datafixers.util.Either; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.CompletableFuture; ++ ++public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected final ChunkAccess fromChunk; ++ protected final ChunkStatus fromStatus; ++ protected final ChunkStatus toStatus; ++ protected final List neighbours; ++ ++ protected final PrioritisedExecutor.PrioritisedTask generateTask; ++ ++ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final ChunkAccess chunk, final List neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.fromChunk = chunk; ++ this.fromStatus = chunk.getStatus(); ++ this.toStatus = toStatus; ++ this.neighbours = neighbours; ++ this.generateTask = (this.toStatus.isParallelCapable ? this.scheduler.parallelGenExecutor : this.scheduler.genExecutor) ++ .createTask(this, priority); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return this.toStatus; ++ } ++ ++ private boolean isEmptyTask() { ++ // must use fromStatus here to avoid any race condition with run() overwriting the status ++ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); ++ return (generation && this.toStatus.isEmptyGenStatus()) || (!generation && this.toStatus.isEmptyLoadStatus()); ++ } ++ ++ @Override ++ public void run() { ++ final ChunkAccess chunk = this.fromChunk; ++ ++ final ServerChunkCache serverChunkCache = this.world.chunkSource; ++ final ChunkMap chunkMap = serverChunkCache.chunkMap; ++ ++ final CompletableFuture> completeFuture; ++ ++ final boolean generation; ++ boolean completing = false; ++ ++ // note: should optimise the case where the chunk does not need to execute the status, because ++ // schedule() calls this synchronously if it will run through that path ++ ++ try { ++ generation = !chunk.getStatus().isOrAfter(this.toStatus); ++ if (generation) { ++ if (this.toStatus.isEmptyGenStatus()) { ++ if (chunk instanceof ProtoChunk) { ++ ((ProtoChunk)chunk).setStatus(this.toStatus); ++ } ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = this.toStatus.generate(Runnable::run, this.world, chunkMap.generator, chunkMap.structureTemplateManager, ++ serverChunkCache.getLightEngine(), null, this.neighbours, false) ++ .whenComplete((final Either either, final Throwable throwable) -> { ++ final ChunkAccess newChunk = (either == null) ? null : either.left().orElse(null); ++ if (newChunk instanceof ProtoChunk) { ++ ((ProtoChunk)newChunk).setStatus(ChunkUpgradeGenericStatusTask.this.toStatus); ++ } ++ } ++ ); ++ } else { ++ if (this.toStatus.isEmptyLoadStatus()) { ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = this.toStatus.load(this.world, chunkMap.structureTemplateManager, serverChunkCache.getLightEngine(), null, chunk); ++ } ++ } catch (final Throwable throwable) { ++ if (!completing) { ++ this.complete(null, throwable); ++ ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), ++ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), ++ "Generation task", this ++ ), throwable); ++ ++ if (!(throwable instanceof ThreadDeath)) { ++ LOGGER.error("Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), world: " + this.world.getWorld().getName(), throwable); ++ } else { ++ // ensure the chunk system can respond, then die ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ if (!completeFuture.isDone() && !this.toStatus.warnedAboutNoImmediateComplete.getAndSet(true)) { ++ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); ++ } ++ ++ final Either either; ++ final ChunkAccess newChunk; ++ ++ try { ++ either = completeFuture.join(); ++ newChunk = (either == null) ? null : either.left().orElse(null); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ // ensure the chunk system can respond, then die ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ if (newChunk == null) { ++ this.complete(null, new IllegalStateException("Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + ", generation: " + generation + " should not be null! Either: " + either).fillInStackTrace()); ++ return; ++ } ++ ++ this.complete(newChunk, null); ++ } ++ ++ protected volatile boolean scheduled; ++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ if (this.isEmptyTask()) { ++ if (this.generateTask.cancel()) { ++ this.run(); ++ } ++ } else { ++ this.generateTask.queue(); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.generateTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.generateTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BiConsumer; ++ ++public abstract class GenericDataLoadTask { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); ++ ++ // reference count is the upper 32 bits ++ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); ++ ++ protected static final long STAGE_MASK = 0xFFFFFFFFL; ++ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; ++ protected static final long STAGE_NOT_STARTED = 0L; ++ protected static final long STAGE_LOADING = 1L; ++ protected static final long STAGE_PROCESSING = 2L; ++ protected static final long STAGE_COMPLETED = 3L; ++ ++ // for loading data off disk ++ protected final LoadDataFromDiskTask loadDataFromDiskTask; ++ // processing off-main ++ protected final PrioritisedExecutor.PrioritisedTask processOffMain; ++ // processing on-main ++ protected final PrioritisedExecutor.PrioritisedTask processOnMain; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ protected final RegionFileIOThread.RegionFileType type; ++ ++ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ ++ final ProcessOnMainTask mainTask; ++ if (this.hasOnMain()) { ++ mainTask = new ProcessOnMainTask(); ++ this.processOnMain = this.createOnMain(mainTask, priority); ++ } else { ++ mainTask = null; ++ this.processOnMain = null; ++ } ++ ++ final ProcessOffMainTask offMainTask; ++ if (this.hasOffMain()) { ++ offMainTask = new ProcessOffMainTask(mainTask); ++ this.processOffMain = this.createOffMain(offMainTask, priority); ++ } else { ++ offMainTask = null; ++ this.processOffMain = null; ++ } ++ ++ if (this.processOffMain == null && this.processOnMain == null) { ++ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); ++ } ++ ++ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); ++ } ++ ++ public static final record TaskResult(L left, R right) {} ++ ++ protected abstract boolean hasOffMain(); ++ ++ protected abstract boolean hasOnMain(); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract TaskResult runOffMain(final CompoundTag data, final Throwable throwable); ++ ++ protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); ++ ++ protected abstract void onComplete(final TaskResult result); ++ ++ protected abstract TaskResult completeOnMainOffMain(final OnMain data, final Throwable throwable); ++ ++ @Override ++ public String toString() { ++ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + this.world.getWorld().getName() + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", type: " + this.type.toString() + "}"; ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ if (this.processOnMain != null) { ++ return this.processOnMain.getPriority(); ++ } else { ++ return this.processOffMain.getPriority(); ++ } ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ if (this.processOffMain != null) { ++ this.processOffMain.lowerPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.setPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.raisePriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.raisePriority(priority); ++ } ++ } ++ ++ // returns whether scheduleNow() needs to be called ++ public boolean schedule(final boolean delay) { ++ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || ++ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { ++ // try and increment reference count ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { ++ // cancelled or completed, nothing to do here ++ return false; ++ } ++ ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { ++ // successful ++ return false; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ if (!delay) { ++ this.scheduleNow(); ++ return false; ++ } ++ return true; ++ } ++ ++ public void scheduleNow() { ++ this.loadDataFromDiskTask.schedule(); // will schedule the rest ++ } ++ ++ // assumes the current stage cannot be completed ++ // returns false if cancelled, returns true if can proceed ++ private boolean advanceStage(final long expect, final long to) { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) != expect) { ++ // must be cancelled ++ return false; ++ } ++ ++ final long newVal = (curr & ~STAGE_MASK) | to; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public boolean cancel() { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { ++ return false; ++ } ++ ++ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { ++ // no other references, so we can cancel ++ final long newVal = STAGE_CANCELLED; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ this.loadDataFromDiskTask.cancel(); ++ if (this.processOffMain != null) { ++ this.processOffMain.cancel(); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.cancel(); ++ } ++ this.onComplete(null); ++ return true; ++ } ++ } else { ++ if ((curr & ~STAGE_MASK) == (0L << 32)) { ++ throw new IllegalStateException("Reference count cannot be zero here"); ++ } ++ // just decrease the reference count ++ final long newVal = curr - (1L << 32); ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return false; ++ } ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected final class DataLoadCallback implements BiConsumer { ++ ++ protected final ProcessOffMainTask offMainTask; ++ protected final ProcessOnMainTask onMainTask; ++ ++ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { ++ this.offMainTask = offMainTask; ++ this.onMainTask = onMainTask; ++ } ++ ++ @Override ++ public void accept(final CompoundTag compoundTag, final Throwable throwable) { ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ try { ++ if (compoundTag == CANCELLED_DATA) { ++ // cancelled, except this isn't possible ++ LOGGER.error("Data callback says cancelled, but stage does not?"); ++ return; ++ } ++ ++ // get off of the regionfile callback ASAP, no clue what locks are held right now... ++ if (GenericDataLoadTask.this.processOffMain != null) { ++ this.offMainTask.data = compoundTag; ++ this.offMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOffMain.queue(); ++ return; ++ } else { ++ // no off-main task, so go straight to main ++ this.onMainTask.data = (OnMain)compoundTag; ++ this.onMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); ++ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( ++ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( ++ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2); ++ } ++ } ++ } ++ ++ protected final class ProcessOffMainTask implements Runnable { ++ ++ protected CompoundTag data; ++ protected Throwable throwable; ++ protected final ProcessOnMainTask schedule; ++ ++ public ProcessOffMainTask(final ProcessOnMainTask schedule) { ++ this.schedule = schedule; ++ } ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { ++ // cancelled ++ return; ++ } ++ final TaskResult newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); ++ ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ if (this.schedule != null) { ++ final TaskResult syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); ++ ++ if (syncComplete != null) { ++ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ GenericDataLoadTask.this.onComplete(syncComplete); ++ } // else: cancelled ++ return; ++ } ++ ++ this.schedule.data = newData.left; ++ this.schedule.throwable = newData.right; ++ ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } else { ++ GenericDataLoadTask.this.onComplete((TaskResult)newData); ++ } ++ } ++ } ++ ++ protected final class ProcessOnMainTask implements Runnable { ++ ++ protected OnMain data; ++ protected Throwable throwable; ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ // cancelled ++ return; ++ } ++ final TaskResult result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); ++ ++ GenericDataLoadTask.this.onComplete(result); ++ } ++ } ++ ++ public static final class LoadDataFromDiskTask { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); ++ ++ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; ++ protected static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; ++ protected static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; ++ ++ protected static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ private final ServerLevel world; ++ private final int chunkX; ++ private final int chunkZ; ++ ++ private final RegionFileIOThread.RegionFileType type; ++ private Cancellable dataLoadTask; ++ private Cancellable dataUnloadCancellable; ++ private DelayedPrioritisedTask dataUnloadTask; ++ ++ private final BiConsumer onComplete; ++ ++ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does ++ // hold a priority lock. ++ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileIOThread.RegionFileType type, ++ final BiConsumer onComplete, ++ final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ this.onComplete = onComplete; ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ private void complete(final CompoundTag data, final Throwable throwable) { ++ try { ++ this.onComplete.accept(data, throwable); ++ } catch (final Throwable thr2) { ++ this.world.chunkTaskScheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), ++ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) ++ ), thr2); ++ if (thr2 instanceof ThreadDeath) { ++ throw (ThreadDeath)thr2; ++ } ++ } ++ } ++ ++ protected boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ protected boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.lowerPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.raisePriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed already ++ return; ++ } ++ ++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and ++ // the write to dataLoadTask will check for the cancelled bit ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ ++ this.complete(CANCELLED_DATA, null); ++ } ++ ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { ++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need ++ // to mark complete here to ensure we do not double complete ++ if (LoadDataFromDiskTask.this.markExecuting()) { ++ LoadDataFromDiskTask.this.complete(data, thr); ++ } // else: cancelled ++ }; ++ ++ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); ++ boolean scheduledUnload = false; ++ ++ final NewChunkHolder holder = this.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); ++ if (holder != null) { ++ final BiConsumer unloadConsumer = (final CompoundTag data, final Throwable thr) -> { ++ if (data != null) { ++ consumer.accept(data, null); ++ } else { ++ // need to schedule task ++ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); ++ } ++ }; ++ Cancellable unloadCancellable = null; ++ CompoundTag syncComplete = null; ++ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists ++ final Completable unloadCompletable = unloadTask == null ? null : unloadTask.completable(); ++ if (unloadCompletable != null) { ++ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); ++ if (unloadCancellable == null) { ++ syncComplete = unloadCompletable.getResult(); ++ } ++ } ++ ++ if (syncComplete != null) { ++ consumer.accept(syncComplete, null); ++ return; ++ } ++ ++ if (unloadCancellable != null) { ++ scheduledUnload = true; ++ this.dataUnloadCancellable = unloadCancellable; ++ this.dataUnloadTask = unloadTask.task(); ++ } ++ } ++ ++ this.schedule(scheduledUnload, consumer, initialPriority); ++ } ++ ++ private void schedule(final boolean scheduledUnload, final BiConsumer consumer, final PrioritisedExecutor.Priority initialPriority) { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ if (!scheduledUnload) { ++ this.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ this.world, this.chunkX, this.chunkZ, this.type, consumer, ++ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority ++ ); ++ } ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ return; ++ } ++ ++ if (scheduledUnload) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ } else { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ /* ++ private static final class LoadDataPriorityHolder extends PriorityHolder { ++ ++ protected final LoadDataFromDiskTask task; ++ ++ protected LoadDataPriorityHolder(final PrioritisedExecutor.Priority priority, final LoadDataFromDiskTask task) { ++ super(priority); ++ this.task = task; ++ } ++ ++ @Override ++ protected void cancelScheduled() { ++ final Cancellable dataLoadTask = this.task.dataLoadTask; ++ if (dataLoadTask != null) { ++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and ++ // the write to dataLoadTask will check for the cancelled bit ++ this.task.dataLoadTask.cancel(); ++ } ++ this.task.complete(CANCELLED_DATA, null); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.Priority getScheduledPriority() { ++ final LoadDataFromDiskTask task = this.task; ++ return RegionFileIOThread.getPriority(task.world, task.chunkX, task.chunkZ, task.type); ++ } ++ ++ @Override ++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { ++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need ++ // to mark complete here to ensure we do not double complete ++ if (LoadDataPriorityHolder.this.markExecuting()) { ++ LoadDataPriorityHolder.this.task.complete(data, thr); ++ } // else: cancelled ++ }; ++ task.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ task.world, task.chunkX, task.chunkZ, task.type, consumer, ++ priority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), priority ++ ); ++ if (this.isMarkedExecuted()) { ++ // if we are marked as completed, it could be: ++ // 1. we were cancelled ++ // 2. the consumer was completed ++ // in the 2nd case, cancel() does nothing ++ // in the 1st case, we ensure cancel() is called as it is possible for the cancelling thread ++ // to miss the field write here ++ task.dataLoadTask.cancel(); ++ } ++ } ++ ++ @Override ++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.lowerPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ ++ @Override ++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.setPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ ++ @Override ++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.raisePriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ } ++ */ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ChunkStatus; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.Consumer; ++ ++public final class NewChunkHolder { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final Thread.UncaughtExceptionHandler CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() { ++ @Override ++ public void uncaughtException(final Thread thread, final Throwable throwable) { ++ if (!(throwable instanceof ThreadDeath)) { ++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); ++ } ++ } ++ }; ++ ++ public final ServerLevel world; ++ public final int chunkX; ++ public final int chunkZ; ++ ++ public final ChunkTaskScheduler scheduler; ++ ++ // load/unload state ++ ++ // chunk data state ++ ++ private ChunkEntitySlices entityChunk; ++ // entity chunk that is loaded, but not yet deserialized ++ private CompoundTag pendingEntityChunk; ++ ++ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); ++ final CompoundTag entityChunk; ++ final ChunkEntitySlices ret; ++ this.scheduler.schedulingLock.lock(); ++ try { ++ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { ++ return this.entityChunk; ++ } ++ final CompoundTag pendingEntityChunk = this.pendingEntityChunk; ++ if (!transientChunk && pendingEntityChunk == null) { ++ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); ++ } ++ ++ if (this.entityChunk == null) { ++ ret = this.entityChunk = new ChunkEntitySlices( ++ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), ++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ ); ++ ++ ret.setTransient(transientChunk); ++ ++ this.world.getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); ++ } else { ++ // transientChunk = false here ++ ret = this.entityChunk; ++ this.entityChunk.setTransient(false); ++ } ++ ++ if (!transientChunk) { ++ this.pendingEntityChunk = null; ++ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; ++ } else { ++ entityChunk = null; ++ } ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ ++ if (!transientChunk) { ++ if (entityChunk != null) { ++ final List entities = EntityStorage.readEntities(this.world, entityChunk); ++ ++ this.world.getEntityLookup().addEntityChunkEntities(entities); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ ++ // been read from disk ++ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); ++ ++ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List entityDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { ++ return this.entityDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized ++ // or added into the world (or even into entityChunk) ++ public boolean isEntityChunkNBTLoaded() { ++ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; ++ } ++ ++ private void completeEntityLoad(final GenericDataLoadTask.TaskResult result) { ++ final List completeWaiters; ++ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; ++ boolean scheduleEntityTask = false; ++ this.scheduler.schedulingLock.lock(); ++ try { ++ final List waiters = this.entityDataLoadTaskWaiters; ++ this.entityDataLoadTask = null; ++ if (result != null) { ++ this.entityDataLoadTaskWaiters = null; ++ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.entityDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ entityDataLoadTask.addCallback(this::completeEntityLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ scheduleEntityTask |= entityDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ ++ if (scheduleEntityTask) { ++ entityDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.accept(result); ++ } ++ } ++ ++ this.scheduler.schedulingLock.lock(); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer> consumer) { ++ if (this.isEntityChunkNBTLoaded()) { ++ throw new IllegalStateException("Cannot load entity data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! ++ if (!this.scheduler.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.entityDataLoadTask == null) { ++ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ this.entityDataLoadTask.addCallback(this::completeEntityLoad); ++ this.entityDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.entityDataLoadTaskWaiters.add(ret); ++ if (this.entityDataLoadTask.schedule(true)) { ++ ret.schedule = this.entityDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public EntityDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.entityDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.entityDataLoadTask.cancel(); ++ } ++ } ++ ++ private PoiChunk poiChunk; ++ ++ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List poiDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { ++ return this.poiDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ public boolean isPoiChunkLoaded() { ++ return this.poiChunk != null; ++ } ++ ++ private void completePoiLoad(final GenericDataLoadTask.TaskResult result) { ++ final List completeWaiters; ++ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; ++ boolean schedulePoiTask = false; ++ this.scheduler.schedulingLock.lock(); ++ try { ++ final List waiters = this.poiDataLoadTaskWaiters; ++ this.poiDataLoadTask = null; ++ if (result != null) { ++ this.poiDataLoadTaskWaiters = null; ++ this.poiChunk = result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.poiDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ poiDataLoadTask.addCallback(this::completePoiLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ schedulePoiTask |= poiDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ ++ if (schedulePoiTask) { ++ poiDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.accept(result); ++ } ++ } ++ this.scheduler.schedulingLock.lock(); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer> consumer) { ++ if (this.isPoiChunkLoaded()) { ++ throw new IllegalStateException("Cannot load poi data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! ++ if (!this.scheduler.schedulingLock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.poiDataLoadTask == null) { ++ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ this.poiDataLoadTask.addCallback(this::completePoiLoad); ++ this.poiDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.poiDataLoadTaskWaiters.add(ret); ++ if (this.poiDataLoadTask.schedule(true)) { ++ ret.schedule = this.poiDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public PoiDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.poiDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.poiDataLoadTask.cancel(); ++ } ++ } ++ ++ public static abstract class GenericDataLoadTaskCallback implements Cancellable, Consumer> { ++ ++ protected final Consumer> consumer; ++ protected final NewChunkHolder chunkHolder; ++ protected boolean completed; ++ protected GenericDataLoadTask schedule; ++ protected final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public GenericDataLoadTaskCallback(final Consumer> consumer, ++ final NewChunkHolder chunkHolder) { ++ this.consumer = consumer; ++ this.chunkHolder = chunkHolder; ++ } ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double calling schedule()"); ++ } ++ if (this.schedule != null) { ++ this.schedule.scheduleNow(); ++ this.schedule = null; ++ } ++ } ++ ++ boolean isCompleted() { ++ return this.completed; ++ } ++ ++ // must hold scheduling lock ++ private boolean setCompleted() { ++ if (this.completed) { ++ return false; ++ } ++ return this.completed = true; ++ } ++ ++ @Override ++ public void accept(final GenericDataLoadTask.TaskResult result) { ++ if (result != null) { ++ if (this.setCompleted()) { ++ this.consumer.accept(result); ++ } else { ++ throw new IllegalStateException("Cannot be cancelled at this point"); ++ } ++ } else { ++ throw new NullPointerException("Result cannot be null (cancelled)"); ++ } ++ } ++ ++ // holds scheduling lock ++ abstract void internalCancel(); ++ ++ @Override ++ public boolean cancel() { ++ this.chunkHolder.scheduler.schedulingLock.lock(); ++ try { ++ if (!this.completed) { ++ this.completed = true; ++ this.internalCancel(); ++ return true; ++ } ++ return false; ++ } finally { ++ this.chunkHolder.scheduler.schedulingLock.unlock(); ++ } ++ } ++ } ++ ++ private ChunkAccess currentChunk; ++ ++ // generation status state ++ ++ /** ++ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all ++ */ ++ private ChunkStatus currentGenStatus; ++ ++ // This allows unsynchronised access to the chunk and last gen status ++ private volatile ChunkCompletion lastChunkCompletion; ++ ++ public ChunkCompletion getLastChunkCompletion() { ++ return this.lastChunkCompletion; ++ } ++ ++ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; ++ ++ /** ++ * The target final chunk status the chunk system will bring the chunk to. ++ */ ++ private ChunkStatus requestedGenStatus; ++ ++ private ChunkProgressionTask generationTask; ++ private ChunkStatus generationTaskStatus; ++ ++ /** ++ * contains the neighbours that this chunk generation is blocking on ++ */ ++ protected final ReferenceLinkedOpenHashSet neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); ++ ++ /** ++ * map of ChunkHolder -> Required Status for this chunk ++ */ ++ protected final Reference2ObjectLinkedOpenHashMap neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); ++ ++ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { ++ this.neighboursBlockingGenTask.add(neighbour); ++ } ++ ++ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { ++ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); ++ this.neighboursWaitingForUs.put(neighbour, requiredStatus); ++ if (wasEmpty) { ++ this.checkUnload(); ++ } ++ } ++ ++ // priority state ++ ++ // the target priority for this chunk to generate at ++ // TODO this will screw over scheduling at lower priorities to neighbours, fix ++ private PrioritisedExecutor.Priority priority = PrioritisedExecutor.Priority.NORMAL; ++ private boolean priorityLocked; ++ ++ // the priority neighbouring chunks have requested this chunk generate at ++ private PrioritisedExecutor.Priority neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; ++ ++ public PrioritisedExecutor.Priority getEffectivePriority() { ++ return PrioritisedExecutor.Priority.max(this.priority, this.neighbourRequestedPriority); ++ } ++ ++ protected void recalculateNeighbourRequestedPriority() { ++ if (this.neighboursWaitingForUs.isEmpty()) { ++ this.neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; ++ return; ++ } ++ ++ PrioritisedExecutor.Priority max = PrioritisedExecutor.Priority.IDLE; ++ ++ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { ++ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(); ++ if (neighbourPriority.isHigherPriority(max)) { ++ max = neighbourPriority; ++ } ++ } ++ ++ final PrioritisedExecutor.Priority current = this.getEffectivePriority(); ++ this.neighbourRequestedPriority = max; ++ final PrioritisedExecutor.Priority next = this.getEffectivePriority(); ++ ++ if (current == next) { ++ return; ++ } ++ ++ // our effective priority has changed, so change our task ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(next); ++ } ++ ++ // now propagate this to our neighbours ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ public void recalculateNeighbourPriorities() { ++ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { ++ holder.recalculateNeighbourRequestedPriority(); ++ } ++ } ++ ++ // must hold scheduling lock ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ private void lockPriority() { ++ this.priority = PrioritisedExecutor.Priority.NORMAL; ++ this.priorityLocked = true; ++ } ++ ++ // must hold scheduling lock ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priorityLocked) { ++ return; ++ } ++ final PrioritisedExecutor.Priority old = this.getEffectivePriority(); ++ this.priority = priority; ++ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(); ++ ++ if (old != newPriority) { ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(newPriority); ++ } ++ } ++ ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ // must hold scheduling lock ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ // error handling state ++ private ChunkStatus failedGenStatus; ++ private Throwable genTaskException; ++ private Thread genTaskFailedThread; ++ ++ private boolean failedLightUpdate; ++ ++ public void failedLightUpdate() { ++ this.failedLightUpdate = true; ++ } ++ ++ public boolean hasFailedGeneration() { ++ return this.genTaskException != null; ++ } ++ ++ // ticket level state ++ private int oldTicketLevel = ChunkMap.MAX_CHUNK_DISTANCE + 1; ++ private int currentTicketLevel = ChunkMap.MAX_CHUNK_DISTANCE + 1; ++ ++ public int getTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ public final ChunkHolder vanillaChunkHolder; ++ ++ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.scheduler = scheduler; ++ this.vanillaChunkHolder = new ChunkHolder(new ChunkPos(chunkX, chunkZ), world, world.getLightEngine(), world.chunkSource.chunkMap, this); ++ } ++ ++ protected ImposterProtoChunk wrappedChunkForNeighbour; ++ ++ // holds scheduling lock ++ public ChunkAccess getChunkForNeighbourAccess() { ++ // Vanilla overrides the status futures with an imposter chunk to prevent writes to full chunks ++ // But we don't store per-status futures, so we need this hack ++ if (this.wrappedChunkForNeighbour != null) { ++ return this.wrappedChunkForNeighbour; ++ } ++ final ChunkAccess ret = this.currentChunk; ++ return ret instanceof LevelChunk fullChunk ? this.wrappedChunkForNeighbour = new ImposterProtoChunk(fullChunk, false) : ret; ++ } ++ ++ public ChunkAccess getCurrentChunk() { ++ return this.currentChunk; ++ } ++ ++ int getCurrentTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ void updateTicketLevel(final int toLevel) { ++ this.currentTicketLevel = toLevel; ++ } ++ ++ private int totalNeighboursUsingThisChunk = 0; ++ ++ // holds schedule lock ++ public void addNeighbourUsingChunk() { ++ final int now = ++this.totalNeighboursUsingThisChunk; ++ ++ if (now == 1) { ++ this.checkUnload(); ++ } ++ } ++ ++ // holds schedule lock ++ public void removeNeighbourUsingChunk() { ++ final int now = --this.totalNeighboursUsingThisChunk; ++ ++ if (now == 0) { ++ this.checkUnload(); ++ } ++ ++ if (now < 0) { ++ throw new IllegalStateException("Neighbours using this chunk cannot be negative"); ++ } ++ } ++ ++ // must hold scheduling lock ++ // returns string reason for why chunk should remain loaded, null otherwise ++ public final String isSafeToUnload() { ++ // is ticket level below threshold? ++ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return "ticket_level"; ++ } ++ ++ // are we being used by another chunk for generation? ++ if (this.totalNeighboursUsingThisChunk != 0) { ++ return "neighbours_generating"; ++ } ++ ++ // are we going to be used by another chunk for generation? ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ return "neighbours_waiting"; ++ } ++ ++ // chunk must be marked inaccessible (i.e unloaded to plugins) ++ if (this.getChunkStatus() != ChunkHolder.FullChunkStatus.INACCESSIBLE) { ++ return "fullchunkstatus"; ++ } ++ ++ // are we currently generating anything, or have requested generation? ++ if (this.generationTask != null) { ++ return "generating"; ++ } ++ if (this.requestedGenStatus != null) { ++ return "requested_generation"; ++ } ++ ++ // entity data requested? ++ if (this.entityDataLoadTask != null) { ++ return "entity_data_requested"; ++ } ++ ++ // poi data requested? ++ if (this.poiDataLoadTask != null) { ++ return "poi_data_requested"; ++ } ++ ++ // are we pending serialization? ++ if (this.entityDataUnload != null) { ++ return "entity_serialization"; ++ } ++ if (this.poiDataUnload != null) { ++ return "poi_serialization"; ++ } ++ if (this.chunkDataUnload != null) { ++ return "chunk_serialization"; ++ } ++ ++ // Note: light tasks do not need a check, as they add a ticket. ++ ++ // nothing is using this chunk, so it should be unloaded ++ return null; ++ } ++ ++ /** Unloaded from chunk map */ ++ boolean killed; ++ ++ // must hold scheduling lock ++ private void checkUnload() { ++ if (this.killed) { ++ return; ++ } ++ if (this.isSafeToUnload() == null) { ++ // ensure in unload queue ++ this.scheduler.chunkHolderManager.unloadQueue.add(this); ++ } else { ++ // ensure not in unload queue ++ this.scheduler.chunkHolderManager.unloadQueue.remove(this); ++ } ++ } ++ ++ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; ++ ++ // note: these are completed with null to indicate that no write occurred ++ // they are also completed with null to indicate a null write occurred ++ private UnloadTask chunkDataUnload; ++ private UnloadTask entityDataUnload; ++ private UnloadTask poiDataUnload; ++ ++ public static final record UnloadTask(Completable completable, DelayedPrioritisedTask task) {} ++ ++ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return this.chunkDataUnload; ++ case ENTITY_DATA: ++ return this.entityDataUnload; ++ case POI_DATA: ++ return this.poiDataUnload; ++ default: ++ throw new IllegalStateException("Unknown regionfile type " + type); ++ } ++ } ++ ++ private UnloadState unloadState; ++ ++ // holds schedule lock ++ UnloadState unloadStage1() { ++ // because we hold the scheduling lock, we cannot actually unload anything ++ // so we need to null this chunk's state ++ ChunkAccess chunk = this.currentChunk; ++ ChunkEntitySlices entityChunk = this.entityChunk; ++ PoiChunk poiChunk = this.poiChunk; ++ // chunk state ++ this.currentChunk = null; ++ this.currentGenStatus = null; ++ this.wrappedChunkForNeighbour = null; ++ this.lastChunkCompletion = null; ++ // entity chunk state ++ this.entityChunk = null; ++ this.pendingEntityChunk = null; ++ ++ // poi chunk state ++ this.poiChunk = null; ++ ++ // priority state ++ this.priorityLocked = false; ++ ++ if (chunk != null) { ++ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); ++ } ++ if (poiChunk != null) { ++ this.poiDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ if (entityChunk != null) { ++ this.entityDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ ++ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; ++ } ++ ++ // data is null if failed or does not need to be saved ++ void completeAsyncChunkDataSave(final CompoundTag data) { ++ if (data != null) { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ this.chunkDataUnload.completable().complete(data); ++ this.scheduler.schedulingLock.lock(); ++ try { ++ // can only write to these fields while holding the schedule lock ++ this.chunkDataUnload = null; ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ } ++ ++ void unloadStage2(final UnloadState state) { ++ this.unloadState = null; ++ final ChunkAccess chunk = state.chunk(); ++ final ChunkEntitySlices entityChunk = state.entityChunk(); ++ final PoiChunk poiChunk = state.poiChunk(); ++ ++ final boolean shouldLevelChunkNotSave = (chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave); ++ ++ // unload chunk data ++ if (chunk != null) { ++ if (chunk instanceof LevelChunk levelChunk) { ++ levelChunk.setLoaded(false); ++ } ++ ++ if (!shouldLevelChunkNotSave) { ++ this.saveChunk(chunk, true); ++ } else { ++ this.completeAsyncChunkDataSave(null); ++ } ++ ++ if (chunk instanceof LevelChunk levelChunk) { ++ this.world.unload(levelChunk); ++ } ++ } ++ ++ // unload entity data ++ if (entityChunk != null) { ++ this.saveEntities(entityChunk, true); ++ // yes this is a hack to pass the compound tag through... ++ final CompoundTag lastEntityUnload = this.lastEntityUnload; ++ this.lastEntityUnload = null; ++ ++ if (entityChunk.unload()) { ++ this.scheduler.schedulingLock.lock(); ++ try { ++ entityChunk.setTransient(true); ++ this.entityChunk = entityChunk; ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ } else { ++ this.world.getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); ++ } ++ // we need to delay the callback until after determining transience, otherwise a potential loader could ++ // set entityChunk before we do ++ this.entityDataUnload.completable().complete(lastEntityUnload); ++ } ++ ++ // unload poi data ++ if (poiChunk != null) { ++ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { ++ this.savePOI(poiChunk, true); ++ } else { ++ this.poiDataUnload.completable().complete(null); ++ } ++ ++ if (poiChunk.isLoaded()) { ++ this.world.getPoiManager().onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ } ++ } ++ } ++ ++ boolean unloadStage3() { ++ // can only write to these while holding the schedule lock, and we instantly complete them in stage2 ++ this.poiDataUnload = null; ++ this.entityDataUnload = null; ++ ++ // we need to check if anything has been loaded in the meantime (or if we have transient entities) ++ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { ++ return false; ++ } ++ ++ return this.isSafeToUnload() == null; ++ } ++ ++ private void cancelGenTask() { ++ if (this.generationTask != null) { ++ this.generationTask.cancel(); ++ } else { ++ // otherwise, we are blocking on neighbours, so remove them ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { ++ if (neighbour.neighboursWaitingForUs.remove(this) == null) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (neighbour.neighboursWaitingForUs.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ } ++ this.neighboursBlockingGenTask.clear(); ++ this.checkUnload(); ++ } ++ } ++ } ++ ++ // holds: ticket level update lock ++ // holds: schedule lock ++ public void processTicketLevelUpdate(final List scheduledTasks, final List changedLoadStatus) { ++ final int oldLevel = this.oldTicketLevel; ++ final int newLevel = this.currentTicketLevel; ++ ++ if (oldLevel == newLevel) { ++ return; ++ } ++ ++ this.oldTicketLevel = newLevel; ++ ++ final ChunkHolder.FullChunkStatus oldState = ChunkHolder.getFullChunkStatus(oldLevel); ++ final ChunkHolder.FullChunkStatus newState = ChunkHolder.getFullChunkStatus(newLevel); ++ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ ++ final ChunkStatus maxGenerationStatusOld = ChunkHolder.getStatus(oldLevel); ++ final ChunkStatus maxGenerationStatusNew = ChunkHolder.getStatus(newLevel); ++ ++ // check for cancellations from downgrading ticket level ++ if (this.requestedGenStatus != null && !newState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && newLevel > oldLevel) { ++ // note: cancel() may invoke onChunkGenComplete synchronously here ++ if (newUnloaded) { ++ // need to cancel all tasks ++ // note: requested status must be set to null here before cancellation, to indicate to the ++ // completion logic that we do not want rescheduling to occur ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ final ChunkStatus toCancel = maxGenerationStatusNew.getNextStatus(); ++ final ChunkStatus currentRequestedStatus = this.requestedGenStatus; ++ ++ if (currentRequestedStatus.isOrAfter(toCancel)) { ++ // we do have to cancel something here ++ // clamp requested status to the maximum ++ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { ++ // already generated to status, so we must cancel ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ // not generated to status, so we may have to cancel ++ // note: gen task is always 1 status above current gen status if not null ++ this.requestedGenStatus = maxGenerationStatusNew; ++ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { ++ // TOOD is this even possible? i don't think so ++ throw new IllegalStateException("?????"); ++ } ++ } ++ } ++ } ++ } ++ ++ if (newState != oldState) { ++ if (newState.isOrAfter(oldState)) { ++ // status upgrade ++ if (!oldState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && newState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ // may need to schedule full load ++ if (this.currentGenStatus != ChunkStatus.FULL) { ++ if (this.requestedGenStatus != null) { ++ this.requestedGenStatus = ChunkStatus.FULL; ++ } else { ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks ++ ); ++ } ++ } else { ++ // now we are fully loaded ++ this.queueBorderFullStatus(true, changedLoadStatus); ++ } ++ } ++ } else { ++ // status downgrade ++ if (!newState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING)) { ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.ENTITY_TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING) && oldState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING)) { ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && oldState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.BORDER, null); ++ } ++ } ++ } ++ ++ if (oldState != newState) { ++ if (this.onTicketUpdate(oldState, newState)) { ++ changedLoadStatus.add(this); ++ } ++ } ++ ++ if (oldUnloaded != newUnloaded) { ++ this.checkUnload(); ++ } ++ } ++ ++ /* ++ For full chunks, vanilla just loads chunks around it up to FEATURES, 1 radius ++ ++ For ticking chunks, it updates the persistent entity manager (soon to be completely nuked by EntitySliceManager, which ++ will also need to be updated but with far less implications) ++ It also shoves the scheduled block ticks into the tick scheduler ++ ++ For entity ticking chunks, updates the entity manager (see above) ++ */ ++ ++ static final int NEIGHBOUR_RADIUS = 2; ++ private long fullNeighbourChunksLoadedBitset; ++ ++ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { ++ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) ++ // optimised variant of the above by moving some of the ops to compile time ++ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); ++ } ++ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; ++ } ++ ++ // returns true if this chunk changed full status ++ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ final long before = this.fullNeighbourChunksLoadedBitset; ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset |= (1L << index); ++ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); ++ } ++ ++ // returns true if this chunk changed full status ++ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { ++ final long before = this.fullNeighbourChunksLoadedBitset; ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset &= ~(1L << index); ++ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); ++ } ++ ++ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { ++ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))) ++ switch (radius) { ++ case 0: { ++ return (bitset & (1L << getFullNeighbourIndex(0, 0))) != 0L; ++ } ++ case 1: { ++ long mask = 0L; ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ mask |= (1L << getFullNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ case 2: { ++ long mask = 0L; ++ for (int dx = -2; dx <= 2; ++dx) { ++ for (int dz = -2; dz <= 2; ++dz) { ++ mask |= (1L << getFullNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ ++ default: { ++ throw new IllegalArgumentException("Radius not recognized: " + radius); ++ } ++ } ++ } ++ ++ // upper 16 bits are pending status, lower 16 bits are current status ++ private volatile long chunkStatus; ++ private static final long PENDING_STATUS_MASK = Long.MIN_VALUE >> 31; ++ private static final ChunkHolder.FullChunkStatus[] CHUNK_STATUS_BY_ID = ChunkHolder.FullChunkStatus.values(); ++ private static final VarHandle CHUNK_STATUS_HANDLE = ConcurrentUtil.getVarHandle(NewChunkHolder.class, "chunkStatus", long.class); ++ ++ public static ChunkHolder.FullChunkStatus getCurrentChunkStatus(final long encoded) { ++ return CHUNK_STATUS_BY_ID[(int)encoded]; ++ } ++ ++ public static ChunkHolder.FullChunkStatus getPendingChunkStatus(final long encoded) { ++ return CHUNK_STATUS_BY_ID[(int)(encoded >>> 32)]; ++ } ++ ++ public ChunkHolder.FullChunkStatus getChunkStatus() { ++ return getCurrentChunkStatus(((long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this))); ++ } ++ ++ public boolean isEntityTickingReady() { ++ return this.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING); ++ } ++ ++ public boolean isTickingReady() { ++ return this.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING); ++ } ++ ++ public boolean isFullChunkReady() { ++ return this.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER); ++ } ++ ++ private static ChunkHolder.FullChunkStatus getStatusForBitset(final long bitset) { ++ if (areNeighboursFullLoaded(bitset, 2)) { ++ return ChunkHolder.FullChunkStatus.ENTITY_TICKING; ++ } else if (areNeighboursFullLoaded(bitset, 1)) { ++ return ChunkHolder.FullChunkStatus.TICKING; ++ } else if (areNeighboursFullLoaded(bitset, 0)) { ++ return ChunkHolder.FullChunkStatus.BORDER; ++ } else { ++ return ChunkHolder.FullChunkStatus.INACCESSIBLE; ++ } ++ } ++ ++ // note: only while updating ticket level, so holds ticket update lock + scheduling lock ++ protected final boolean onTicketUpdate(final ChunkHolder.FullChunkStatus oldState, final ChunkHolder.FullChunkStatus newState) { ++ if (oldState == newState) { ++ return false; ++ } ++ ++ // preserve border request after full status complete, as it does not set anything in the bitset ++ ChunkHolder.FullChunkStatus byNeighbours = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); ++ if (byNeighbours == ChunkHolder.FullChunkStatus.INACCESSIBLE && newState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && this.currentGenStatus == ChunkStatus.FULL) { ++ byNeighbours = ChunkHolder.FullChunkStatus.BORDER; ++ } ++ ++ final ChunkHolder.FullChunkStatus toSet; ++ ++ if (newState.isOrAfter(byNeighbours)) { ++ // must clamp to neighbours level, even though we have the ticket level ++ toSet = byNeighbours; ++ } else { ++ // must clamp to ticket level, even though we have the neighbours ++ toSet = newState; ++ } ++ ++ long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this); ++ ++ if (curr == ((long)toSet.ordinal() | ((long)toSet.ordinal() << 32))) { ++ // nothing to do ++ return false; ++ } ++ ++ int failures = 0; ++ for (;;) { ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toSet.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected final boolean onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { ++ ChunkHolder.FullChunkStatus oldState = getStatusForBitset(bitsetBefore); ++ ChunkHolder.FullChunkStatus newState = getStatusForBitset(bitsetAfter); ++ final ChunkHolder.FullChunkStatus currStateTicketLevel = ChunkHolder.getFullChunkStatus(this.oldTicketLevel); ++ if (oldState.isOrAfter(currStateTicketLevel)) { ++ oldState = currStateTicketLevel; ++ } ++ if (newState.isOrAfter(currStateTicketLevel)) { ++ newState = currStateTicketLevel; ++ } ++ // preserve border request after full status complete, as it does not set anything in the bitset ++ if (newState == ChunkHolder.FullChunkStatus.INACCESSIBLE && currStateTicketLevel.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && this.currentGenStatus == ChunkStatus.FULL) { ++ newState = ChunkHolder.FullChunkStatus.BORDER; ++ } ++ ++ if (oldState == newState) { ++ return false; ++ } ++ ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)newState.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ private boolean queueBorderFullStatus(final boolean loaded, final List changedFullStatus) { ++ final ChunkHolder.FullChunkStatus toStatus = loaded ? ChunkHolder.FullChunkStatus.BORDER : ChunkHolder.FullChunkStatus.INACCESSIBLE; ++ ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final ChunkHolder.FullChunkStatus currPending = getPendingChunkStatus(curr); ++ if (loaded && currPending != ChunkHolder.FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalStateException("Expected " + ChunkHolder.FullChunkStatus.INACCESSIBLE + " for pending, but got " + currPending); ++ } ++ ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toStatus.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ if ((int)(update) != (int)(update >>> 32)) { ++ changedFullStatus.add(this); ++ return true; ++ } ++ return false; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ // only call on main thread, must hold ticket level and scheduling lock ++ private void onFullChunkLoadChange(final boolean loaded, final List changedFullStatus) { ++ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { ++ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { ++ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); ++ if (loaded) { ++ if (holder.setNeighbourFullLoaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } else { ++ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } ++ } ++ } ++ } ++ ++ private ChunkHolder.FullChunkStatus updateCurrentState(final ChunkHolder.FullChunkStatus to) { ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final long update = (curr & PENDING_STATUS_MASK) | (long)to.ordinal(); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return getPendingChunkStatus(curr); ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ private void changeEntityChunkStatus(final ChunkHolder.FullChunkStatus toStatus) { ++ this.world.getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); ++ } ++ ++ private boolean processingFullStatus = false; ++ ++ // only to be called on the main thread, no locks need to be held ++ public boolean handleFullStatusChange(final List changedFullStatus) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); ++ ++ boolean ret = false; ++ ++ if (this.processingFullStatus) { ++ // we cannot process updates recursively ++ return ret; ++ } ++ ++ // note: use opaque reads for chunk status read since we need it to be atomic ++ ++ // test if anything changed ++ final long statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); ++ if ((int)statusCheck == (int)(statusCheck >>> 32)) { ++ // nothing changed ++ return ret; ++ } ++ ++ final ChunkTaskScheduler scheduler = this.scheduler; ++ final ChunkHolderManager holderManager = scheduler.chunkHolderManager; ++ final int ticketKeep; ++ final Long ticketId; ++ holderManager.ticketLock.lock(); ++ try { ++ ticketKeep = this.currentTicketLevel; ++ ticketId = Long.valueOf(holderManager.getNextStatusUpgradeId()); ++ holderManager.addTicketAtLevel(TicketType.STATUS_UPGRADE, this.chunkX, this.chunkZ, ticketKeep, ticketId); ++ } finally { ++ holderManager.ticketLock.unlock(); ++ } ++ ++ this.processingFullStatus = true; ++ try { ++ for (;;) { ++ final long currStateEncoded = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); ++ final ChunkHolder.FullChunkStatus currState = getCurrentChunkStatus(currStateEncoded); ++ ChunkHolder.FullChunkStatus nextState = getPendingChunkStatus(currStateEncoded); ++ if (currState == nextState) { ++ if (nextState == ChunkHolder.FullChunkStatus.INACCESSIBLE) { ++ this.scheduler.schedulingLock.lock(); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ } ++ break; ++ } ++ ++ // chunks cannot downgrade state while status is pending a change ++ final LevelChunk chunk = (LevelChunk)this.currentChunk; ++ ++ // Note: we assume that only load/unload contain plugin logic ++ // plugin logic is anything stupid enough to possibly change the chunk status while it is already ++ // being changed (i.e during load it is possible it will try to set to full ticking) ++ // in order to allow this change, we also need this plugin logic to be contained strictly after all ++ // of the chunk system load callbacks are invoked ++ if (nextState.isOrAfter(currState)) { ++ // state upgrade ++ if (!currState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && nextState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.BORDER); ++ holderManager.ensureInAutosave(this); ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.BORDER); ++ chunk.onChunkLoad(this); ++ this.onFullChunkLoadChange(true, changedFullStatus); ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.BORDER, chunk); ++ } ++ ++ if (!currState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING) && nextState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING)) { ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.TICKING); ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.TICKING); ++ chunk.onChunkTicking(this); ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.TICKING, chunk); ++ } ++ ++ if (!currState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING) && nextState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING)) { ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.ENTITY_TICKING); ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.ENTITY_TICKING); ++ chunk.onChunkEntityTicking(this); ++ this.completeFullStatusConsumers(ChunkHolder.FullChunkStatus.ENTITY_TICKING, chunk); ++ } ++ } else { ++ if (currState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING) && !nextState.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING)) { ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.TICKING); ++ chunk.onChunkNotEntityTicking(this); ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.TICKING); ++ } ++ ++ if (currState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING) && !nextState.isOrAfter(ChunkHolder.FullChunkStatus.TICKING)) { ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.BORDER); ++ chunk.onChunkNotTicking(this); ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.BORDER); ++ } ++ ++ if (currState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !nextState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ this.onFullChunkLoadChange(false, changedFullStatus); ++ this.changeEntityChunkStatus(ChunkHolder.FullChunkStatus.INACCESSIBLE); ++ chunk.onChunkUnload(this); ++ nextState = this.updateCurrentState(ChunkHolder.FullChunkStatus.INACCESSIBLE); ++ } ++ } ++ ++ ret = true; ++ } ++ } finally { ++ this.processingFullStatus = false; ++ holderManager.removeTicketAtLevel(TicketType.STATUS_UPGRADE, this.chunkX, this.chunkZ, ticketKeep, ticketId); ++ } ++ ++ return ret; ++ } ++ ++ // note: must hold scheduling lock ++ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) ++ boolean upgradeGenTarget(final ChunkStatus toStatus) { ++ if (toStatus == null) { ++ throw new NullPointerException("toStatus cannot be null"); ++ } ++ if (this.requestedGenStatus == null && this.generationTask == null) { ++ return false; ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { ++ this.requestedGenStatus = toStatus; ++ } ++ return true; ++ } ++ ++ public void setGenerationTarget(final ChunkStatus toStatus) { ++ this.requestedGenStatus = toStatus; ++ } ++ ++ public boolean hasGenerationTask() { ++ return this.generationTask != null; ++ } ++ ++ public ChunkStatus getCurrentGenStatus() { ++ return this.currentGenStatus; ++ } ++ ++ public ChunkStatus getRequestedGenStatus() { ++ return this.requestedGenStatus; ++ } ++ ++ private final Reference2ObjectOpenHashMap>> statusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addStatusConsumer(final ChunkStatus status, final Consumer consumer) { ++ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { ++ // need to tell future statuses to complete if cancelled ++ do { ++ this.completeStatusConsumers0(status, chunk); ++ } while (chunk == null && status != (status = status.getNextStatus())); ++ } ++ ++ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { ++ final List> consumers; ++ consumers = this.statusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addFullStatusConsumer(final ChunkHolder.FullChunkStatus status, final Consumer consumer) { ++ this.fullStatusWaiters.computeIfAbsent(status, (final ChunkHolder.FullChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeFullStatusConsumers(ChunkHolder.FullChunkStatus status, final LevelChunk chunk) { ++ // need to tell future statuses to complete if cancelled ++ final ChunkHolder.FullChunkStatus max = CHUNK_STATUS_BY_ID[CHUNK_STATUS_BY_ID.length - 1]; ++ ++ for (;;) { ++ this.completeFullStatusConsumers0(status, chunk); ++ if (chunk != null || status == max) { ++ break; ++ } ++ status = CHUNK_STATUS_BY_ID[status.ordinal() + 1]; ++ } ++ } ++ ++ private void completeFullStatusConsumers0(final ChunkHolder.FullChunkStatus status, final LevelChunk chunk) { ++ final List> consumers; ++ consumers = this.fullStatusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ // note: must hold scheduling lock ++ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, ++ final List scheduleList, final List changedLoadStatus) { ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ throw new IllegalStateException("Cannot have neighbours blocking this gen task"); ++ } ++ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { ++ this.completeStatusConsumers(newStatus, newChunk); ++ } ++ // done now, clear state (must be done before scheduling new tasks) ++ this.generationTask = null; ++ this.generationTaskStatus = null; ++ if (newChunk == null) { ++ // task was cancelled ++ // should be careful as this could be called while holding the schedule lock and/or inside the ++ // ticket level update ++ // while a task may be cancelled, it is possible for it to be later re-scheduled ++ // however, because generationTask is only set to null on _completion_, the scheduler leaves ++ // the rescheduling logic to us here ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ this.requestedGenStatus = null; ++ if (requestedGenStatus != null) { ++ // it looks like it has been requested, so we must reschedule ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final Iterator> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry entry = iterator.next(); ++ ++ final NewChunkHolder chunkHolder = entry.getKey(); ++ final ChunkStatus toStatus = entry.getValue(); ++ ++ if (!requestedGenStatus.isOrAfter(toStatus)) { ++ // if we were cancelled, we are responsible for removing the waiter ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ iterator.remove(); ++ continue; ++ } ++ } ++ } ++ ++ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ ++ // return, can't do anything further ++ return; ++ } ++ ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ } ++ this.neighboursWaitingForUs.clear(); ++ } ++ // reset priority, we have nothing left to generate to ++ this.setPriority(PrioritisedExecutor.Priority.NORMAL); ++ this.checkUnload(); ++ return; ++ } ++ ++ this.currentChunk = newChunk; ++ this.currentGenStatus = newStatus; ++ this.lastChunkCompletion = new ChunkCompletion(newChunk, newStatus); ++ ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ ++ List needsScheduling = null; ++ boolean recalculatePriority = false; ++ for (final Iterator> iterator ++ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry entry = iterator.next(); ++ final NewChunkHolder neighbour = entry.getKey(); ++ final ChunkStatus requiredStatus = entry.getValue(); ++ ++ if (!newStatus.isOrAfter(requiredStatus)) { ++ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { ++ // if we're cancelled, still need to clear this map ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ ++ iterator.remove(); ++ } ++ continue; ++ } ++ ++ // doesn't matter what isCancelled is here, we need to schedule if we can ++ ++ recalculatePriority = true; ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ if (neighbour.requestedGenStatus != null) { ++ if (needsScheduling == null) { ++ needsScheduling = new ArrayList<>(); ++ } ++ needsScheduling.add(neighbour); ++ } else { ++ neighbour.checkUnload(); ++ } ++ } ++ ++ // remove last; access to entry will throw if removed ++ iterator.remove(); ++ } ++ ++ if (newStatus == ChunkStatus.FULL) { ++ this.lockPriority(); ++ // must use oldTicketLevel, we hold the schedule lock but not the ticket level lock ++ // however, schedule lock needs to be held for ticket level callback, so we're fine here ++ if (ChunkHolder.getFullChunkStatus(this.oldTicketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { ++ this.queueBorderFullStatus(true, changedLoadStatus); ++ } ++ } ++ ++ if (recalculatePriority) { ++ this.recalculateNeighbourRequestedPriority(); ++ } ++ ++ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ ++ // we need to schedule more tasks now ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ } else { ++ // we're done now ++ if (requestedGenStatus != null) { ++ this.requestedGenStatus = null; ++ } ++ // reached final stage, so stop scheduling now ++ this.setPriority(PrioritisedExecutor.Priority.NORMAL); ++ this.checkUnload(); ++ ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ } ++ } ++ ++ private void scheduleNeighbours(final List needsScheduling, final List scheduleList) { ++ if (needsScheduling != null) { ++ for (int i = 0, len = needsScheduling.size(); i < len; ++i) { ++ final NewChunkHolder neighbour = needsScheduling.get(i); ++ ++ this.scheduler.schedule( ++ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList ++ ); ++ } ++ } ++ } ++ ++ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, ++ final List neighbours) { ++ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { ++ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { ++ throw new IllegalStateException("Cannot schedule generation task when not requested"); ++ } ++ this.generationTask = generationTask; ++ this.generationTaskStatus = taskStatus; ++ ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).addNeighbourUsingChunk(); ++ } ++ ++ this.checkUnload(); ++ ++ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { ++ if (generationTask != this.generationTask) { ++ throw new IllegalStateException( ++ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" ++ ); ++ } ++ if (thr != null) { ++ if (this.genTaskException != null) { ++ // first one is probably the TRUE problem ++ return; ++ } ++ // don't set generation task to null, so that scheduling will not attempt to create another task and it ++ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed ++ // task to complete ++ this.genTaskException = thr; ++ this.failedGenStatus = taskStatus; ++ this.genTaskFailedThread = Thread.currentThread(); ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), ++ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) ++ ), thr); ++ return; ++ } ++ ++ final boolean scheduleTasks; ++ List tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); ++ if (tasks == null) { ++ scheduleTasks = true; ++ tasks = new ArrayList<>(); ++ } else { ++ scheduleTasks = false; ++ // we are currently updating ticket levels, so we already hold the schedule lock ++ // this means we have to leave the ticket level update to handle the scheduling ++ } ++ final List changedLoadStatus = new ArrayList<>(); ++ this.scheduler.schedulingLock.lock(); ++ try { ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).removeNeighbourUsingChunk(); ++ } ++ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); ++ } finally { ++ this.scheduler.schedulingLock.unlock(); ++ } ++ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); ++ ++ if (scheduleTasks) { ++ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ } ++ }); ++ } ++ ++ public PoiChunk getPoiChunk() { ++ return this.poiChunk; ++ } ++ ++ public ChunkEntitySlices getEntityChunk() { ++ return this.entityChunk; ++ } ++ ++ public long lastAutoSave; ++ ++ public boolean save(final boolean shutdown, final boolean unloading) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); ++ ++ ChunkAccess chunk = this.getCurrentChunk(); ++ PoiChunk poi = this.getPoiChunk(); ++ ChunkEntitySlices entities = this.getEntityChunk(); ++ boolean executedUnloadTask = false; ++ ++ if (shutdown) { ++ // make sure that the async unloads complete ++ if (this.unloadState != null) { ++ // must have errored during unload ++ chunk = this.unloadState.chunk(); ++ poi = this.unloadState.poiChunk(); ++ entities = this.unloadState.entityChunk(); ++ } ++ final UnloadTask chunkUnloadTask = this.chunkDataUnload; ++ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); ++ if (chunkDataUnloadTask != null) { ++ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); ++ if (unloadTask != null) { ++ executedUnloadTask = unloadTask.execute(); ++ } ++ } ++ } ++ ++ boolean canSaveChunk = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && ++ (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); ++ boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); ++ boolean canSaveEntities = entities != null; ++ ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper ++ if (canSaveChunk) { ++ canSaveChunk = this.saveChunk(chunk, unloading); ++ } ++ if (canSavePOI) { ++ canSavePOI = this.savePOI(poi, unloading); ++ } ++ if (canSaveEntities) { ++ // on shutdown, we need to force transient entity chunks to save ++ canSaveEntities = this.saveEntities(entities, unloading || shutdown); ++ if (unloading || shutdown) { ++ this.lastEntityUnload = null; ++ } ++ } ++ } ++ ++ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI; ++ } ++ ++ static final class AsyncChunkSerializeTask implements Runnable { ++ ++ private final ServerLevel world; ++ private final ChunkAccess chunk; ++ private final ChunkSerializer.AsyncSaveData asyncSaveData; ++ private final NewChunkHolder toComplete; ++ ++ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final ChunkSerializer.AsyncSaveData asyncSaveData, ++ final NewChunkHolder toComplete) { ++ this.world = world; ++ this.chunk = chunk; ++ this.asyncSaveData = asyncSaveData; ++ this.toComplete = toComplete; ++ } ++ ++ @Override ++ public void run() { ++ final CompoundTag toSerialize; ++ try { ++ toSerialize = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + this.world.getWorld().getName() + "', falling back to synchronous save", throwable); ++ this.world.chunkTaskScheduler.scheduleChunkTask(this.chunk.locX, this.chunk.locZ, () -> { ++ final CompoundTag synchronousSave; ++ try { ++ synchronousSave = ChunkSerializer.saveChunk(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable throwable2) { ++ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "', chunk data will be lost", throwable2); ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(null); ++ return; ++ } ++ ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(synchronousSave); ++ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "' synchronously"); ++ ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ return; ++ } ++ this.toComplete.completeAsyncChunkDataSave(toSerialize); ++ } ++ ++ @Override ++ public String toString() { ++ return "AsyncChunkSerializeTask{" + ++ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + this.world.getWorld().getName() + "\"}" + ++ "}"; ++ } ++ } ++ ++ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { ++ if (!chunk.isUnsaved()) { ++ if (unloading) { ++ this.completeAsyncChunkDataSave(null); ++ } ++ return false; ++ } ++ boolean completing = false; ++ try { ++ if (unloading) { ++ try { ++ final ChunkSerializer.AsyncSaveData asyncSaveData = ChunkSerializer.getAsyncSaveData(this.world, chunk); ++ ++ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); ++ ++ this.chunkDataUnload.task().setTask(task); ++ ++ task.queue(); ++ ++ return true; ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', falling back to synchronous save", thr); ++ // fall through to synchronous save ++ } ++ } ++ ++ final CompoundTag save = ChunkSerializer.saveChunk(this.world, chunk, null); ++ ++ if (unloading) { ++ completing = true; ++ this.completeAsyncChunkDataSave(save); ++ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "' synchronously"); ++ } else { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ if (unloading && !completing) { ++ this.completeAsyncChunkDataSave(null); ++ } ++ } ++ ++ return true; ++ } ++ ++ private boolean lastEntitySaveNull; ++ private CompoundTag lastEntityUnload; ++ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { ++ try { ++ CompoundTag mergeFrom = null; ++ if (entities.isTransient()) { ++ if (!unloading) { ++ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will ++ // result in double adding the entities ++ return false; ++ } ++ try { ++ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); ++ } catch (final Exception ex) { ++ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', data on disk will be replaced", ex); ++ } ++ } ++ ++ final CompoundTag save = entities.save(); ++ if (mergeFrom != null) { ++ if (save == null) { ++ // don't override the data on disk with nothing ++ return false; ++ } else { ++ EntityStorage.copyEntities(mergeFrom, save); ++ } ++ } ++ if (save == null && this.lastEntitySaveNull) { ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ this.lastEntitySaveNull = save == null; ++ if (unloading) { ++ this.lastEntityUnload = save; ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ return true; ++ } ++ ++ private boolean lastPoiSaveNull; ++ private boolean savePOI(final PoiChunk poi, final boolean unloading) { ++ try { ++ final CompoundTag save = poi.save(); ++ poi.setDirty(false); ++ if (save == null && this.lastPoiSaveNull) { ++ if (unloading) { ++ this.poiDataUnload.completable().complete(null); ++ } ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); ++ this.lastPoiSaveNull = save == null; ++ if (unloading) { ++ this.poiDataUnload.completable().complete(save); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices entityChunk = this.entityChunk; ++ final long chunkStatus = this.chunkStatus; ++ final int fullChunkStatus = (int)chunkStatus; ++ final int pendingChunkStatus = (int)(chunkStatus >>> 32); ++ final ChunkHolder.FullChunkStatus currentFullStatus = fullChunkStatus < 0 || fullChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[fullChunkStatus]; ++ final ChunkHolder.FullChunkStatus pendingFullStatus = pendingChunkStatus < 0 || pendingChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[pendingChunkStatus]; ++ return "NewChunkHolder{" + ++ "world=" + this.world.getWorld().getName() + ++ ", chunkX=" + this.chunkX + ++ ", chunkZ=" + this.chunkZ + ++ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + ++ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + ++ ", currentGenStatus=" + this.currentGenStatus + ++ ", requestedGenStatus=" + this.requestedGenStatus + ++ ", generationTask=" + this.generationTask + ++ ", generationTaskStatus=" + this.generationTaskStatus + ++ ", priority=" + this.priority + ++ ", priorityLocked=" + this.priorityLocked + ++ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + ++ ", effective_priority=" + this.getEffectivePriority() + ++ ", oldTicketLevel=" + this.oldTicketLevel + ++ ", currentTicketLevel=" + this.currentTicketLevel + ++ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + ++ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + ++ ", chunkStatusRaw=" + chunkStatus + ++ ", currentChunkStatus=" + currentFullStatus + ++ ", pendingChunkStatus=" + pendingFullStatus + ++ ", is_unload_safe=" + this.isSafeToUnload() + ++ ", killed=" + this.killed + ++ '}'; ++ } ++ ++ private static JsonElement serializeCompletable(final Completable completable) { ++ if (completable == null) { ++ return new JsonPrimitive("null"); ++ } ++ ++ final JsonObject ret = new JsonObject(); ++ final boolean isCompleted = completable.isCompleted(); ++ ret.addProperty("completed", Boolean.valueOf(isCompleted)); ++ ++ if (isCompleted) { ++ ret.addProperty("completed_exceptionally", Boolean.valueOf(completable.getThrowable() != null)); ++ } ++ ++ return ret; ++ } ++ ++ // holds ticket and scheduling lock ++ public JsonObject getDebugJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices slices = this.entityChunk; ++ final PoiChunk poiChunk = this.poiChunk; ++ ++ ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); ++ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); ++ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); ++ ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); ++ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); ++ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); ++ ret.addProperty("priority", Objects.toString(this.priority)); ++ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); ++ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); ++ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); ++ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); ++ ++ final JsonObject neighbourWaitState = new JsonObject(); ++ ret.add("neighbour_state", neighbourWaitState); ++ ++ final JsonArray blockingGenNeighbours = new JsonArray(); ++ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); ++ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { ++ final JsonObject neighbour = new JsonObject(); ++ blockingGenNeighbours.add(neighbour); ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); ++ } ++ ++ final JsonArray neighboursWaitingForUs = new JsonArray(); ++ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); ++ for (final Reference2ObjectMap.Entry entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { ++ final NewChunkHolder holder = entry.getKey(); ++ final ChunkStatus status = entry.getValue(); ++ ++ final JsonObject neighbour = new JsonObject(); ++ neighboursWaitingForUs.add(neighbour); ++ ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); ++ neighbour.addProperty("waiting_for", Objects.toString(status)); ++ } ++ ++ ret.addProperty("fullchunkstatus", Objects.toString(this.getChunkStatus())); ++ ret.addProperty("fullchunkstatus_raw", Long.valueOf(this.chunkStatus)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); ++ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); ++ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); ++ ++ final UnloadTask entityDataUnload = this.entityDataUnload; ++ final UnloadTask poiDataUnload = this.poiDataUnload; ++ final UnloadTask chunkDataUnload = this.chunkDataUnload; ++ ++ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); ++ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); ++ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); ++ ++ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); ++ if (unloadTask == null) { ++ ret.addProperty("unload_task_priority", "null"); ++ ret.addProperty("unload_task_priority_raw", "null"); ++ } else { ++ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); ++ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); ++ } ++ ++ ret.addProperty("killed", Boolean.valueOf(this.killed)); ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++ ++public abstract class PriorityHolder { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); ++ ++ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; ++ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); ++ } ++ ++ protected PriorityHolder(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ // used only for debug json ++ public boolean isScheduled() { ++ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; ++ } ++ ++ // returns false if cancelled ++ protected boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ protected boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled already ++ return; ++ } ++ this.cancelScheduled(); ++ } ++ ++ public void schedule() { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final PrioritisedExecutor.Priority getPriority() { ++ final int ret = this.getPriorityVolatile(); ++ if ((ret & PRIORITY_EXECUTED) != 0) { ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ if ((ret & PRIORITY_SCHEDULED) != 0) { ++ return this.getScheduledPriority(); ++ } ++ return PrioritisedExecutor.Priority.getPriority(ret); ++ } ++ ++ public final void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.lowerPriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isLowerPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.setPriorityScheduled(priority); ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.raisePriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isHigherPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected abstract void cancelScheduled(); ++ ++ protected abstract PrioritisedExecutor.Priority getScheduledPriority(); ++ ++ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); ++} +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.FixLightCommand; + import io.papermc.paper.command.subcommands.HeapDumpCommand; +@@ -0,0 +0,0 @@ public final class PaperCommand extends Command { + commands.put(Set.of("reload"), new ReloadCommand()); + commands.put(Set.of("version"), new VersionCommand()); + commands.put(Set.of("fixlight"), new FixLightCommand()); ++ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), 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 net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++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); ++ case "holderinfo" -> this.doHolderInfo(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 "holderinfo" -> { ++ 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); ++ } ++ } ++ 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 doHolderInfo(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 accumulatedCanUnload = 0; ++ int accumulatedNull = 0; ++ int accumulatedReadOnly = 0; ++ int accumulatedProtoChunk = 0; ++ int accumulatedFullChunk = 0; ++ ++ for (final org.bukkit.World bukkitWorld : worlds) { ++ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ ++ int total = 0; ++ int canUnload = 0; ++ int nullChunks = 0; ++ int readOnly = 0; ++ int protoChunk = 0; ++ int fullChunk = 0; ++ ++ for (final ChunkHolder chunk : world.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders()) { // Paper - change updating chunks map ++ final ChunkAccess lastChunk = chunk.getAvailableChunkNow(); ++ ++ ++total; ++ ++ if (lastChunk == null) { ++ ++nullChunks; ++ } else if (lastChunk instanceof ImposterProtoChunk) { ++ ++readOnly; ++ } else if (lastChunk instanceof ProtoChunk) { ++ ++protoChunk; ++ } else if (lastChunk instanceof LevelChunk) { ++ ++fullChunk; ++ } ++ ++ if (chunk.newChunkHolder.isSafeToUnload() == null) { ++ ++canUnload; ++ } ++ } ++ ++ accumulatedTotal += total; ++ accumulatedCanUnload += canUnload; ++ accumulatedNull += nullChunks; ++ accumulatedReadOnly += readOnly; ++ accumulatedProtoChunk += protoChunk; ++ accumulatedFullChunk += fullChunk; ++ ++ 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(" Unloadable: ", BLUE), text(canUnload), ++ text(" Null: ", BLUE), text(nullChunks), ++ text(" ReadOnly: ", BLUE), text(readOnly), ++ text(" Proto: ", BLUE), text(protoChunk), ++ text(" Full: ", BLUE), text(fullChunk) ++ )); ++ } ++ 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(" Unloadable: ", BLUE), text(accumulatedCanUnload), ++ text(" Null: ", BLUE), text(accumulatedNull), ++ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), ++ text(" Proto: ", BLUE), text(accumulatedProtoChunk), ++ text(" Full: ", BLUE), text(accumulatedFullChunk) ++ )); ++ } ++ } ++ ++ 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, false); ++ 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/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/io/papermc/paper/util/TickThread.java ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -0,0 +0,0 @@ import net.minecraft.world.entity.Entity; + import org.bukkit.Bukkit; + import java.util.concurrent.atomic.AtomicInteger; + +-public final class TickThread extends Thread { ++public class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); + +@@ -0,0 +0,0 @@ public final class TickThread extends Thread { + } + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void softEnsureTickThread(final String reason) { + if (!STRICT_THREAD_CHECKS) { + return; +@@ -0,0 +0,0 @@ public final class TickThread extends Thread { + ensureTickThread(reason); + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -0,0 +0,0 @@ public final class TickThread extends Thread { + } + + public static boolean isTickThread() { +- return Bukkit.isPrimaryThread(); ++ return Thread.currentThread() instanceof TickThread; + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { +- return Bukkit.isPrimaryThread(); ++ return Thread.currentThread() instanceof TickThread; + } + + public static boolean isTickThreadFor(final Entity entity) { +- return Bukkit.isPrimaryThread(); ++ return Thread.currentThread() instanceof TickThread; + } + } +diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.world; ++ ++import com.destroystokyo.paper.util.maplist.EntityList; ++import io.papermc.paper.chunk.system.entity.EntityLookup; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.boss.EnderDragonPart; ++import net.minecraft.world.entity.boss.enderdragon.EnderDragon; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.entity.Visibility; ++import net.minecraft.world.phys.AABB; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Predicate; ++ ++public final class ChunkEntitySlices { ++ ++ protected final int minSection; ++ protected final int maxSection; ++ public final int chunkX; ++ public final int chunkZ; ++ protected final ServerLevel world; ++ ++ protected final EntityCollectionBySection allEntities; ++ protected final EntityCollectionBySection hardCollidingEntities; ++ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; ++ protected final EntityList entities = new EntityList(); ++ ++ public ChunkHolder.FullChunkStatus status; ++ ++ protected boolean isTransient; ++ ++ public boolean isTransient() { ++ return this.isTransient; ++ } ++ ++ public void setTransient(final boolean value) { ++ this.isTransient = value; ++ } ++ ++ // TODO implement container search optimisations ++ ++ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus status, ++ final int minSection, final int maxSection) { // inclusive, inclusive ++ this.minSection = minSection; ++ this.maxSection = maxSection; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.world = world; ++ ++ this.allEntities = new EntityCollectionBySection(this); ++ this.hardCollidingEntities = new EntityCollectionBySection(this); ++ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); ++ ++ this.status = status; ++ } ++ ++ // Paper start - optimise CraftChunk#getEntities ++ public org.bukkit.entity.Entity[] getChunkEntities() { ++ List ret = new java.util.ArrayList<>(); ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ if (entity == null) { ++ continue; ++ } ++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); ++ if (bukkit != null && bukkit.isValid()) { ++ ret.add(bukkit); ++ } ++ } ++ ++ return ret.toArray(new org.bukkit.entity.Entity[0]); ++ } ++ ++ public CompoundTag save() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return null; ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = rawData[i]; ++ if (entity.shouldBeSaved()) { ++ collectedEntities.add(entity); ++ } ++ } ++ ++ if (collectedEntities.isEmpty()) { ++ return null; ++ } ++ ++ return EntityStorage.saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), this.world); ++ } ++ ++ // returns true if this chunk has transient entities remaining ++ public boolean unload() { ++ final int len = this.entities.size(); ++ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); ++ ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = collectedEntities[i]; ++ if (entity.isRemoved()) { ++ // removed by us below ++ continue; ++ } ++ if (entity.shouldBeSaved()) { ++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK); ++ if (entity.isVehicle()) { ++ // we cannot assume that these entities are contained within this chunk, because entities can ++ // desync - so we need to remove them all ++ for (final Entity passenger : entity.getIndirectPassengers()) { ++ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK); ++ } ++ } ++ } ++ } ++ ++ return this.entities.size() != 0; ++ } ++ ++ private List getAllEntities() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return new ArrayList<>(); ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ collectedEntities.add(rawData[i]); ++ } ++ ++ return collectedEntities; ++ } ++ ++ public void callEntitiesLoadEvent() { ++ CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ ++ public void callEntitiesUnloadEvent() { ++ CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ // Paper end - optimise CraftChunk#getEntities ++ ++ public boolean isEmpty() { ++ return this.entities.size() == 0; ++ } ++ ++ public void mergeInto(final ChunkEntitySlices slices) { ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ slices.addEntity(entity, entity.sectionY); ++ } ++ } ++ ++ private boolean preventStatusUpdates; ++ public boolean startPreventingStatusUpdates() { ++ final boolean ret = this.preventStatusUpdates; ++ this.preventStatusUpdates = true; ++ return ret; ++ } ++ ++ public void stopPreventingStatusUpdates(final boolean prev) { ++ this.preventStatusUpdates = prev; ++ } ++ ++ public void updateStatus(final ChunkHolder.FullChunkStatus status, final EntityLookup lookup) { ++ this.status = status; ++ ++ final Entity[] entities = this.entities.getRawData(); ++ ++ for (int i = 0, size = this.entities.size(); i < size; ++i) { ++ final Entity entity = entities[i]; ++ ++ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); ++ entity.chunkStatus = status; ++ final Visibility newVisibility = EntityLookup.getEntityStatus(entity); ++ ++ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); ++ } ++ } ++ ++ public boolean addEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.add(entity)) { ++ return false; ++ } ++ entity.chunkStatus = this.status; ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.addEntity(entity, sectionIndex); ++ ++ if (entity.hardCollides()) { ++ this.hardCollidingEntities.addEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().addEntity(entity, sectionIndex); ++ } ++ } ++ ++ return true; ++ } ++ ++ public boolean removeEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.remove(entity)) { ++ return false; ++ } ++ entity.chunkStatus = null; ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.removeEntity(entity, sectionIndex); ++ ++ if (entity.hardCollides()) { ++ this.hardCollidingEntities.removeEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().removeEntity(entity, sectionIndex); ++ } ++ } ++ ++ return true; ++ } ++ ++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.hardCollidingEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); ++ } ++ ++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ ++ protected EntityCollectionBySection initClass(final Class clazz) { ++ final EntityCollectionBySection ret = new EntityCollectionBySection(this); ++ ++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { ++ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; ++ if (sectionEntities == null) { ++ continue; ++ } ++ ++ final Entity[] storage = sectionEntities.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (clazz.isInstance(entity)) { ++ ret.addEntity(entity, sectionIndex); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } else { ++ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } ++ } ++ ++ protected static final class BasicEntityList { ++ ++ protected static final Entity[] EMPTY = new Entity[0]; ++ protected static final int DEFAULT_CAPACITY = 4; ++ ++ protected E[] storage; ++ protected int size; ++ ++ public BasicEntityList() { ++ this(0); ++ } ++ ++ public BasicEntityList(final int cap) { ++ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); ++ } ++ ++ public boolean isEmpty() { ++ return this.size == 0; ++ } ++ ++ public int size() { ++ return this.size; ++ } ++ ++ private void resize() { ++ if (this.storage == EMPTY) { ++ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; ++ } else { ++ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); ++ } ++ } ++ ++ public void add(final E entity) { ++ final int idx = this.size++; ++ if (idx >= this.storage.length) { ++ this.resize(); ++ this.storage[idx] = entity; ++ } else { ++ this.storage[idx] = entity; ++ } ++ } ++ ++ public int indexOf(final E entity) { ++ final E[] storage = this.storage; ++ ++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { ++ if (storage[i] == entity) { ++ return i; ++ } ++ } ++ ++ return -1; ++ } ++ ++ public boolean remove(final E entity) { ++ final int idx = this.indexOf(entity); ++ if (idx == -1) { ++ return false; ++ } ++ ++ final int size = --this.size; ++ final E[] storage = this.storage; ++ if (idx != size) { ++ System.arraycopy(storage, idx + 1, storage, idx, size - idx); ++ } ++ ++ storage[size] = null; ++ ++ return true; ++ } ++ ++ public boolean has(final E entity) { ++ return this.indexOf(entity) != -1; ++ } ++ } ++ ++ protected static final class EntityCollectionBySection { ++ ++ protected final ChunkEntitySlices manager; ++ protected final long[] nonEmptyBitset; ++ protected final BasicEntityList[] entitiesBySection; ++ protected int count; ++ ++ public EntityCollectionBySection(final ChunkEntitySlices manager) { ++ this.manager = manager; ++ ++ final int sectionCount = manager.maxSection - manager.minSection + 1; ++ ++ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE ++ this.entitiesBySection = new BasicEntityList[sectionCount]; ++ } ++ ++ public void addEntity(final Entity entity, final int sectionIndex) { ++ BasicEntityList list = this.entitiesBySection[sectionIndex]; ++ ++ if (list != null && list.has(entity)) { ++ return; ++ } ++ ++ if (list == null) { ++ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); ++ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); ++ } ++ ++ list.add(entity); ++ ++this.count; ++ } ++ ++ public void removeEntity(final Entity entity, final int sectionIndex) { ++ final BasicEntityList list = this.entitiesBySection[sectionIndex]; ++ ++ if (list == null || !list.remove(entity)) { ++ return; ++ } ++ ++ --this.count; ++ ++ if (list.isEmpty()) { ++ this.entitiesBySection[sectionIndex] = null; ++ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); ++ } ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(entity)) { ++ continue; ++ } ++ ++ into.add(entity); ++ } ++ } ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { ++ if (part == except || !part.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { ++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test((T)entity)) { ++ continue; ++ } ++ ++ into.add((T)entity); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- 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 start - add pending task queue ++ private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); ++ public void execute(final Runnable run) { ++ if (this.channel == null || !this.channel.isRegistered()) { ++ run.run(); ++ return; ++ } ++ final boolean queue = !this.queue.isEmpty(); ++ if (!queue) { ++ this.channel.eventLoop().execute(run); ++ } else { ++ this.pendingTasks.add(run); ++ if (this.queue.isEmpty()) { ++ // something flushed async, dump tasks now ++ Runnable r; ++ while ((r = this.pendingTasks.poll()) != null) { ++ this.channel.eventLoop().execute(r); ++ } ++ } ++ } ++ } ++ // Paper end - add pending task queue + + public Connection(PacketFlow side) { + this.receiving = side; +@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + private void flushQueue() { ++ try { // Paper - add pending task queue + if (this.channel != null && this.channel.isOpen()) { + Queue queue = this.queue; + +@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> { + + } + } ++ } finally { // Paper start - add pending task queue ++ Runnable r; ++ while ((r = this.pendingTasks.poll()) != null) { ++ this.channel.eventLoop().execute(r); ++ } ++ } // Paper end - add pending task queue + } + + public void tick() { +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 onComplete) { +- if (gen) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- return; +- } +- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { +- if (chunk == null) { +- onComplete.accept(null); +- } else { +- if (chunk.getStatus().isOrAfter(toStatus)) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- } else { +- onComplete.accept(null); +- } +- } +- }); ++ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + +- static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); +- +- private static long chunkLoadCounter = 0L; ++ // Paper - rewrite chunk system + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 + ChunkStatus.getDistance(toStatus); +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer loadCallback = (final ChunkAccess chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap); +- +- if (loadFuture.isDone()) { +- loadCallback.accept(loadFuture.join().left().orElse(null)); +- return; +- } +- +- loadFuture.whenCompleteAsync((final Either either, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(either.left().orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final ChunkHolder.FullChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- if (toStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { +- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); +- } +- +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 - (toStatus.ordinal() - 1); +- final int radius = toStatus.ordinal() - 1; +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer loadCallback = (final LevelChunk chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture> tickingState; +- switch (toStatus) { +- case BORDER: { +- tickingState = holder.getFullChunkFuture(); +- break; +- } +- case TICKING: { +- tickingState = holder.getTickingChunkFuture(); +- break; +- } +- case ENTITY_TICKING: { +- tickingState = holder.getEntityTickingChunkFuture(); +- break; +- } +- default: { +- throw new IllegalStateException("Cannot reach here"); +- } +- } +- +- if (tickingState.isDone()) { +- loadCallback.accept(tickingState.join().left().orElse(null)); +- return; +- } +- +- tickingState.whenCompleteAsync((final Either either, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(either.left().orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ level.chunkTaskScheduler.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + + public static List getVisibleChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system + } + + public static List getUpdatingChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); ++ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system + } + + public static int getVisibleChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.visibleChunkMap.size(); ++ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system + } + + public static int getUpdatingChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.updatingChunkMap.size(); ++ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system + } + + public static boolean hasAnyChunkHolders(final ServerLevel level) { +@@ -0,0 +0,0 @@ public final class ChunkSystem { + } + + public static int getSendViewDistance(final ServerPlayer player) { +- return getLoadViewDistance(player); ++ return io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.getLevel(); +- if (level == null) { +- return Bukkit.getViewDistance() + 1; +- } +- return level.chunkSource.chunkMap.getEffectiveViewDistance() + 1; ++ return io.papermc.paper.chunk.PlayerChunkLoader.getLoadViewDistance(player); + } + + public static int getTickViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.getLevel(); +- if (level == null) { +- return Bukkit.getSimulationDistance(); +- } +- return level.chunkSource.chunkMap.distanceManager.getSimulationDistance(); ++ return io.papermc.paper.chunk.PlayerChunkLoader.getTickViewDistance(player); + } + + private ChunkSystem() { +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 it.unimi.dsi.fastutil.objects.ReferenceArrayList; + 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, boolean watchdog) throws IOException { ++ file.getParentFile().mkdirs(); ++ file.createNewFile(); ++ ReferenceArrayList worlds = new ReferenceArrayList<>(org.bukkit.Bukkit.getWorlds()); ++ ReferenceArrayList loadedWorlds = new ReferenceArrayList<>(worlds); ++ JsonObject data = new JsonObject(); ++ ++ data.addProperty("server-version", org.bukkit.Bukkit.getVersion()); ++ data.addProperty("data-version", 1); ++ ++ { ++ JsonArray players = new JsonArray(); ++ data.add("all-players", players); ++ List playerList = MinecraftServer.getServer().getPlayerList().players; ++ for (ServerPlayer player : playerList) { ++ JsonObject playerData = new JsonObject(); ++ players.add(playerData); ++ ++ Level playerWorld = player.getLevel(); ++ org.bukkit.World craftWorld = playerWorld.getWorld(); ++ Entity.RemovalReason removalReason = player.getRemovalReason(); ++ ++ playerData.addProperty("name", player.getScoreboardName()); ++ playerData.addProperty("x", player.getX()); ++ playerData.addProperty("y", player.getY()); ++ playerData.addProperty("z", player.getZ()); ++ playerData.addProperty("world", playerWorld == null ? "null world" : craftWorld.getName()); ++ playerData.addProperty("removalReason", removalReason == null ? "null" : removalReason.name()); ++ ++ if (!worlds.contains(craftWorld)) { ++ worlds.add(craftWorld); ++ } ++ } ++ } ++ ++ JsonArray chunkWaitInformation = new JsonArray(); ++ data.add("chunk-wait-infos", chunkWaitInformation); ++ ++ for (io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.ChunkInfo chunkInfo : io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.getChunkInfos()) { ++ chunkWaitInformation.add(chunkInfo.toString()); ++ } ++ ++ JsonArray worldsData = new JsonArray(); ++ ++ for (org.bukkit.World bukkitWorld : worlds) { ++ JsonObject worldData = new JsonObject(); ++ ++ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle(); ++ List players = world.players; ++ ++ worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld)); ++ worldData.addProperty("name", world.getWorld().getName()); ++ worldData.addProperty("view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace chunk loader system ++ worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system ++ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory); ++ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16); ++ ++ 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); ++ worldData.add("chunk-data", watchdog ? world.chunkTaskScheduler.chunkHolderManager.getDebugJsonForWatchdog() : world.chunkTaskScheduler.chunkHolderManager.getDebugJson()); ++ 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/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -0,0 +0,0 @@ public class Main { + + convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata); + */ ++ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName());// Paper - load this sync so it won't fail later async + final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> { + 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 S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +- Thread thread = new Thread(() -> { ++ Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + ((MinecraftServer) atomicreference.get()).runServer(); + }, "Server thread"); + +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- return worldserver1.getChunkSource().chunkMap.hasWork(); +- })) { +- this.nextTickTime = Util.getMillis() + 1L; +- iterator = this.getAllLevels().iterator(); +- +- while (iterator.hasNext()) { +- worldserver = (ServerLevel) iterator.next(); +- worldserver.getChunkSource().removeTicketsOnClosing(); +- worldserver.getChunkSource().tick(() -> { +- return true; +- }, false); +- } +- +- this.waitUntilNextTick(); +- } +- +- this.saveAllChunks(false, true, false); +- iterator = this.getAllLevels().iterator(); +- +- while (iterator.hasNext()) { +- worldserver = (ServerLevel) iterator.next(); +- if (worldserver != null) { +- try { +- worldserver.close(); +- } catch (IOException ioexception) { +- MinecraftServer.LOGGER.error("Exception closing the level", ioexception); +- } +- } +- } ++ this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system - move closing into here + + this.isSaving = false; + this.resources.close(); +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 5000L && this.nextTickTime - this.lastOverloadWarning >= 30000L) { // CraftBukkit +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ LOGGER.info("Async debug chunks executing"); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(false); ++ CommandSender sender = MinecraftServer.getServer().console; ++ java.io.File file = new java.io.File(new java.io.File(new java.io.File("."), "debug"), ++ "chunks-" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(java.time.LocalDateTime.now()) + ".txt"); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ try { ++ net.minecraft.server.MCUtil.dumpChunks(file, true); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); ++ } ++ }; ++ Thread t = new Thread(run); ++ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); ++ t.setDaemon(true); ++ t.start(); ++ return; ++ } ++ // Paper end - rewrite chunk system + this.consoleInput.add(new ConsoleInput(command, commandSource)); + } + +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 static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); + private static final ChunkHolder.FullChunkStatus[] FULL_CHUNK_STATUSES = ChunkHolder.FullChunkStatus.values(); + private static final int BLOCKS_BEFORE_RESEND_FUDGE = 64; +- private final AtomicReferenceArray>> futures; ++ // Paper - rewrite chunk system + private final LevelHeightAccessor levelHeightAccessor; +- private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage +- public CompletableFuture chunkToSave; // Paper - public ++ // Paper - rewrite chunk system + @Nullable + private final DebugBuffer chunkToSaveHistory; +- public int oldTicketLevel; +- private int ticketLevel; +- private int queueLevel; ++ // Paper - rewrite chunk system + public final ChunkPos pos; + private boolean hasChangedSections; + private final ShortSet[] changedBlocksPerSection; +@@ -0,0 +0,0 @@ public class ChunkHolder { + private final LevelLightEngine lightEngine; + private final ChunkHolder.LevelChangeListener onLevelChange; + public final ChunkHolder.PlayerProvider playerProvider; +- private boolean wasAccessibleSinceLastSave; ++ // Paper - rewrite chunk system + private boolean resendLight; +- private CompletableFuture pendingFullStateConfirmation; ++ // Paper - rewrite chunk system + + private final ChunkMap chunkMap; // Paper ++ // Paper start - no-tick view distance ++ public final LevelChunk getSendingChunk() { ++ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used ++ // in Chunk's neighbour callback ++ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z); ++ if (ret != null && ret.areNeighboursLoaded(1)) { ++ return ret; ++ } ++ return null; ++ } ++ // Paper end - no-tick view distance + + // Paper start + public void onChunkAdd() { +@@ -0,0 +0,0 @@ public class ChunkHolder { + } + // Paper end + +- public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { +- this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.chunkToSave = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error ++ public final io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder; // Paper - rewrite chunk system ++ ++ public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system ++ this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system + this.chunkToSaveHistory = null; + this.blockChangedLightSectionFilter = new BitSet(); + this.skyChangedLightSectionFilter = new BitSet(); +- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error ++ // Paper - rewrite chunk system + this.pos = pos; + this.levelHeightAccessor = world; + this.lightEngine = lightingProvider; +- this.onLevelChange = levelUpdateListener; ++ this.onLevelChange = null; // Paper - rewrite chunk system + this.playerProvider = playersWatchingChunkProvider; +- this.oldTicketLevel = ChunkMap.MAX_CHUNK_DISTANCE + 1; +- this.ticketLevel = this.oldTicketLevel; +- this.queueLevel = this.oldTicketLevel; +- this.setTicketLevel(level); ++ // Paper - rewrite chunk system + this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; + this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper + } + + // Paper start + public @Nullable ChunkAccess getAvailableChunkNow() { +- // TODO can we just getStatusFuture(EMPTY)? +- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { +- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); +- Either either = future.getNow(null); +- if (either == null || either.left().isEmpty()) { +- continue; +- } +- return either.left().get(); +- } +- return null; ++ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system + } + // Paper end + // CraftBukkit start + public LevelChunk getFullChunkNow() { +- // Note: We use the oldTicketLevel for isLoaded checks. +- if (!ChunkHolder.getFullChunkStatus(this.oldTicketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) return null; +- return this.getFullChunkNowUnchecked(); ++ // Paper start - rewrite chunk system ++ ChunkAccess chunk = this.getAvailableChunkNow(); ++ if (!this.isFullChunkReady() || !(chunk instanceof LevelChunk)) return null; // instanceof to avoid a race condition on off-main threads ++ return (LevelChunk)chunk; ++ // Paper end - rewrite chunk system + } + + public LevelChunk getFullChunkNowUnchecked() { +- CompletableFuture> statusFuture = this.getFutureIfPresentUnchecked(ChunkStatus.FULL); +- Either either = (Either) statusFuture.getNow(null); +- return (either == null) ? null : (LevelChunk) either.left().orElse(null); ++ // Paper start - rewrite chunk system ++ ChunkAccess chunk = this.getAvailableChunkNow(); ++ return chunk instanceof LevelChunk ? (LevelChunk)chunk : null; ++ // Paper end - rewrite chunk system + } + // CraftBukkit end + + public CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus leastStatus) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(leastStatus.getIndex()); +- +- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> getFutureIfPresent(ChunkStatus leastStatus) { +- return ChunkHolder.getStatus(this.ticketLevel).isOrAfter(leastStatus) ? this.getFutureIfPresentUnchecked(leastStatus) : ChunkHolder.UNLOADED_CHUNK_FUTURE; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture> getTickingChunkFuture() { // Paper - final for inline +- return this.tickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture> getEntityTickingChunkFuture() { // Paper - final for inline +- return this.entityTickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture> getFullChunkFuture() { // Paper - final for inline +- return this.fullChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + public final LevelChunk getTickingChunk() { // Paper - final for inline +- CompletableFuture> completablefuture = this.getTickingChunkFuture(); +- Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error +- +- return either == null ? null : (LevelChunk) either.left().orElse(null); // CraftBukkit - decompile error ++ // Paper start - rewrite chunk system ++ if (!this.isTickingReady()) { ++ return null; ++ } ++ return (LevelChunk)this.getAvailableChunkNow(); ++ // Paper end - rewrite chunk system + } + + @Nullable + public final LevelChunk getFullChunk() { // Paper - final for inline +- CompletableFuture> completablefuture = this.getFullChunkFuture(); +- Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error +- +- return either == null ? null : (LevelChunk) either.left().orElse(null); // CraftBukkit - decompile error ++ // Paper start - rewrite chunk system ++ if (!this.isFullChunkReady()) { ++ return null; ++ } ++ return (LevelChunk)this.getAvailableChunkNow(); ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkStatus getLastAvailableStatus() { +- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { +- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); +- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); +- +- if (((Either) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).left().isPresent()) { +- return chunkstatus; +- } +- } +- +- return null; ++ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system + } + + // Paper start + public ChunkStatus getChunkHolderStatus() { +- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { +- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); +- Either either = future.getNow(null); +- if (either == null || !either.left().isPresent()) { +- continue; +- } +- return curr; +- } +- +- return null; ++ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system + } + // Paper end + + @Nullable + public ChunkAccess getLastAvailable() { +- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { +- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); +- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); +- +- if (!completablefuture.isCompletedExceptionally()) { +- Optional optional = ((Either) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).left(); +- +- if (optional.isPresent()) { +- return (ChunkAccess) optional.get(); +- } +- } +- } +- +- return null; ++ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system + } + +- public final CompletableFuture getChunkToSave() { // Paper - final for inline +- return this.chunkToSave; +- } ++ // Paper - rewrite chunk system + + public void blockChanged(BlockPos pos) { +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { + int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); +@@ -0,0 +0,0 @@ public class ChunkHolder { + } + + public void sectionLightChanged(LightLayer lightType, int y) { +- Either either = (Either) this.getFutureIfPresent(ChunkStatus.FEATURES).getNow(null); // CraftBukkit - decompile error ++ // Paper start - no-tick view distance + +- if (either != null) { +- ChunkAccess ichunkaccess = (ChunkAccess) either.left().orElse(null); // CraftBukkit - decompile error ++ if (true) { ++ ChunkAccess ichunkaccess = this.getAvailableChunkNow(); + + if (ichunkaccess != null) { + ichunkaccess.setUnsaved(true); +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.getSendingChunk(); ++ // Paper end - no-tick view distance + + if (chunk != null) { + int j = this.lightEngine.getMinLightSection(); +@@ -0,0 +0,0 @@ public class ChunkHolder { + } + + public void broadcast(Packet packet, boolean onlyOnWatchDistanceEdge) { +- this.playerProvider.getPlayers(this.pos, onlyOnWatchDistanceEdge).forEach((entityplayer) -> { +- entityplayer.connection.send(packet); +- }); +- } +- +- public CompletableFuture> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) { +- int i = targetStatus.getIndex(); +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); +- +- if (completablefuture != null) { +- Either either = (Either) completablefuture.getNow(ChunkHolder.NOT_DONE_YET); +- +- if (either == null) { +- String s = "value in future for status: " + targetStatus + " was incorrectly set to null at chunk: " + this.pos; ++ // Paper start - per player view distance ++ // there can be potential desync with player's last mapped section and the view distance map, so use the ++ // view distance map here. ++ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos); ++ if (players == null) { ++ return; ++ } + +- throw chunkStorage.debugFuturesAndCreateReportedException(new IllegalStateException("null value previously set for chunk status"), s); ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof ServerPlayer)) { ++ continue; + } +- +- if (either == ChunkHolder.NOT_DONE_YET || either.right().isEmpty()) { +- return completablefuture; ++ ServerPlayer player = (ServerPlayer)temp; ++ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { ++ continue; + } ++ player.connection.send(packet); + } +- +- if (ChunkHolder.getStatus(this.ticketLevel).isOrAfter(targetStatus)) { +- CompletableFuture> completablefuture1 = chunkStorage.schedule(this, targetStatus); +- +- this.updateChunkToSave(completablefuture1, "schedule " + targetStatus); +- this.futures.set(i, completablefuture1); +- return completablefuture1; +- } else { +- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; +- } ++ // Paper end - per player view distance + } + +- protected void addSaveDependency(String thenDesc, CompletableFuture then) { +- if (this.chunkToSaveHistory != null) { +- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); +- } +- +- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, object) -> { +- return ichunkaccess; +- }); +- } +- +- private void updateChunkToSave(CompletableFuture> then, String thenDesc) { +- if (this.chunkToSaveHistory != null) { +- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); +- } +- +- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, either) -> { +- return (ChunkAccess) either.map((ichunkaccess1) -> { +- return ichunkaccess1; +- }, (playerchunk_failure) -> { +- return ichunkaccess; +- }); +- }); +- } ++ // Paper - rewrite chunk system + + public ChunkHolder.FullChunkStatus getFullStatus() { +- return ChunkHolder.getFullChunkStatus(this.ticketLevel); ++ return this.newChunkHolder.getChunkStatus(); // Paper - rewrite chunk system + } + + public final ChunkPos getPos() { // Paper - final for inline +@@ -0,0 +0,0 @@ public class ChunkHolder { + } + + public final int getTicketLevel() { // Paper - final for inline +- return this.ticketLevel; +- } +- +- public int getQueueLevel() { +- return this.queueLevel; +- } +- +- private void setQueueLevel(int level) { +- this.queueLevel = level; +- } +- +- public void setTicketLevel(int level) { +- this.ticketLevel = level; +- } +- +- private void scheduleFullChunkPromotion(ChunkMap playerchunkmap, CompletableFuture> completablefuture, Executor executor, ChunkHolder.FullChunkStatus playerchunk_state) { +- this.pendingFullStateConfirmation.cancel(false); +- CompletableFuture completablefuture1 = new CompletableFuture(); +- +- completablefuture1.thenRunAsync(() -> { +- playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); +- }, executor); +- this.pendingFullStateConfirmation = completablefuture1; +- completablefuture.thenAccept((either) -> { +- either.ifLeft((chunk) -> { +- completablefuture1.complete(null); // CraftBukkit - decompile error +- }); +- }); ++ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system + } + +- private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) { +- this.pendingFullStateConfirmation.cancel(false); +- playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); +- } +- +- protected void updateFutures(ChunkMap chunkStorage, Executor executor) { +- ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel); +- ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel); +- boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; +- boolean flag1 = this.ticketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; +- ChunkHolder.FullChunkStatus playerchunk_state = ChunkHolder.getFullChunkStatus(this.oldTicketLevel); +- ChunkHolder.FullChunkStatus playerchunk_state1 = ChunkHolder.getFullChunkStatus(this.ticketLevel); +- // CraftBukkit start +- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. +- if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { +- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk)either.left().orElse(null); +- if (chunk != null) { +- chunkStorage.callbackExecutor.execute(() -> { +- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick +- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. +- // These actions may however happen deferred, so we manually set the needsSaving flag already here. +- chunk.setUnsaved(true); +- chunk.unloadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkStorage.callbackExecutor.run(); +- } +- // CraftBukkit end +- +- if (flag) { +- Either either = Either.right(new ChunkHolder.ChunkLoadingFailure() { +- public String toString() { +- return "Unloaded ticket level " + ChunkHolder.this.pos; +- } +- }); +- +- for (int i = flag1 ? chunkstatus1.getIndex() + 1 : 0; i <= chunkstatus.getIndex(); ++i) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); +- +- if (completablefuture == null) { +- this.futures.set(i, CompletableFuture.completedFuture(either)); +- } +- } +- } +- +- boolean flag2 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); +- boolean flag3 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); +- +- this.wasAccessibleSinceLastSave |= flag3; +- if (!flag2 && flag3) { +- int expectCreateCount = ++this.fullChunkCreateCount; // Paper +- this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); +- this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); +- // Paper start - cache ticking ready status +- this.fullChunkFuture.thenAccept(either -> { +- final Optional left = either.left(); +- if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { +- LevelChunk fullChunk = either.left().get(); +- ChunkHolder.this.isFullChunkReady = true; +- net.minecraft.server.ChunkSystem.onChunkBorder(fullChunk, this); +- } +- }); +- this.updateChunkToSave(this.fullChunkFuture, "full"); +- } +- +- if (flag2 && !flag3) { +- // Paper start +- if (this.isFullChunkReady) { +- net.minecraft.server.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().left().get(), this); // Paper +- } +- // Paper end +- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- ++this.fullChunkCreateCount; // Paper - cache ticking ready status +- this.isFullChunkReady = false; // Paper - cache ticking ready status +- } +- +- boolean flag4 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.TICKING); +- boolean flag5 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.TICKING); +- +- if (!flag4 && flag5) { +- this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); +- this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); +- // Paper start - cache ticking ready status +- this.tickingChunkFuture.thenAccept(either -> { +- either.ifLeft(chunk -> { +- // note: Here is a very good place to add callbacks to logic waiting on this. +- ChunkHolder.this.isTickingReady = true; +- net.minecraft.server.ChunkSystem.onChunkTicking(chunk, this); +- }); +- }); +- // Paper end +- this.updateChunkToSave(this.tickingChunkFuture, "ticking"); +- } +- +- if (flag4 && !flag5) { +- // Paper start +- if (this.isTickingReady) { +- net.minecraft.server.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().left().get(), this); // Paper +- } +- // Paper end +- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- boolean flag6 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING); +- boolean flag7 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING); +- +- if (!flag6 && flag7) { +- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException()); +- } +- +- this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos); +- this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); +- // Paper start - cache ticking ready status +- this.entityTickingChunkFuture.thenAccept(either -> { +- either.ifLeft(chunk -> { +- ChunkHolder.this.isEntityTickingReady = true; +- net.minecraft.server.ChunkSystem.onChunkEntityTicking(chunk, this); +- }); +- }); +- // Paper end +- this.updateChunkToSave(this.entityTickingChunkFuture, "entity ticking"); +- } +- +- if (flag6 && !flag7) { +- // Paper start +- if (this.isEntityTickingReady) { +- net.minecraft.server.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().left().get(), this); +- } +- // Paper end +- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- if (!playerchunk_state1.isOrAfter(playerchunk_state)) { +- this.demoteFullChunk(chunkStorage, playerchunk_state1); +- } +- +- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); +- this.oldTicketLevel = this.ticketLevel; +- // CraftBukkit start +- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. +- if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { +- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk)either.left().orElse(null); +- if (chunk != null) { +- chunkStorage.callbackExecutor.execute(() -> { +- chunk.loadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkStorage.callbackExecutor.run(); +- } +- // CraftBukkit end +- } ++ // Paper - rewrite chunk system + + public static ChunkStatus getStatus(int level) { + return level < 33 ? ChunkStatus.FULL : ChunkStatus.getStatusAroundFullChunk(level - 33); +@@ -0,0 +0,0 @@ public class ChunkHolder { + return ChunkHolder.FULL_CHUNK_STATUSES[Mth.clamp(33 - distance + 1, (int) 0, ChunkHolder.FULL_CHUNK_STATUSES.length - 1)]; + } + +- public boolean wasAccessibleSinceLastSave() { +- return this.wasAccessibleSinceLastSave; +- } +- +- public void refreshAccessibility() { +- this.wasAccessibleSinceLastSave = ChunkHolder.getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER); +- } ++ // Paper - rewrite chunk system + + public void replaceProtoChunk(ImposterProtoChunk chunk) { +- for (int i = 0; i < this.futures.length(); ++i) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); +- +- if (completablefuture != null) { +- Optional optional = ((Either) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).left(); +- +- if (!optional.isEmpty() && optional.get() instanceof ProtoChunk) { +- this.futures.set(i, CompletableFuture.completedFuture(Either.left(chunk))); +- } +- } +- } +- +- this.updateChunkToSave(CompletableFuture.completedFuture(Either.left(chunk.getWrapped())), "replaceProto"); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public List>>> getAllFutures() { +- List>>> list = new ArrayList(); +- +- for (int i = 0; i < ChunkHolder.CHUNK_STATUSES.size(); ++i) { +- list.add(Pair.of((ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i), (CompletableFuture) this.futures.get(i))); +- } +- +- return list; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @FunctionalInterface +@@ -0,0 +0,0 @@ public class ChunkHolder { + + // Paper start + public final boolean isEntityTickingReady() { +- return this.isEntityTickingReady; ++ return this.newChunkHolder.isEntityTickingReady(); // Paper - rewrite chunk system + } + + public final boolean isTickingReady() { +- return this.isTickingReady; ++ return this.newChunkHolder.isTickingReady(); // Paper - rewrite chunk system + } + + public final boolean isFullChunkReady() { +- return this.isFullChunkReady; ++ return this.newChunkHolder.isFullChunkReady(); // Paper - rewrite chunk system + } + // Paper end + } +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 + public static final int MAX_VIEW_DISTANCE = 33; + public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance(); + public static final int FORCED_TICKET_LEVEL = 31; +- public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); +- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; +- private final Long2ObjectLinkedOpenHashMap pendingUnloads; +- public final LongSet entitiesInLevel; ++ // Paper - rewrite chunk system + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; + public final BlockableEventLoop mainThreadExecutor; // Paper - public +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private RandomState randomState; + public final Supplier overworldDataStorage; + private final PoiManager poiManager; +- public final LongSet toDrop; ++ // Paper - rewrite chunk system + private boolean modified; +- private final ChunkTaskPriorityQueueSorter queueSorter; +- private final ProcessorHandle> worldgenMailbox; +- public final ProcessorHandle> mainThreadMailbox; ++ // Paper - rewrite chunk system + public final ChunkProgressListener progressListener; + private final ChunkStatusUpdateListener chunkStatusListener; + public final ChunkMap.ChunkDistanceManager distanceManager; + private final AtomicInteger tickingGenerated; +- private final StructureTemplateManager structureTemplateManager; ++ public final StructureTemplateManager structureTemplateManager; // Paper - rewrite chunk system + private final String storageName; + private final PlayerMap playerMap; + public final Int2ObjectMap entityMap; +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final Queue unloadQueue; + int viewDistance; + +- // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() +- public final CallbackExecutor callbackExecutor = new CallbackExecutor(); +- public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { +- +- private final java.util.Queue queue = new java.util.ArrayDeque<>(); +- +- @Override +- public void execute(Runnable runnable) { +- this.queue.add(runnable); +- } +- +- @Override +- public void run() { +- Runnable task; +- while ((task = this.queue.poll()) != null) { +- task.run(); +- } +- } +- }; +- // CraftBukkit end ++ // Paper - rewrite chunk system + + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); ++ public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader + + void addPlayerToDistanceMaps(ServerPlayer player) { ++ this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { ++ this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader + + } + +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated ++ this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader + } + // Paper end + // Paper start +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { +- return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ return null; // Paper - rewrite chunk system + } + // Paper end + + 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(); +- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap(); +- this.entitiesInLevel = new LongOpenHashSet(); +- this.toDrop = new LongOpenHashSet(); ++ // Paper - rewrite chunk system + this.tickingGenerated = new AtomicInteger(); + this.playerMap = new PlayerMap(); + this.entityMap = new Int2ObjectOpenHashMap(); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + this.mainThreadExecutor = mainThreadExecutor; +- ProcessorMailbox threadedmailbox = ProcessorMailbox.create(executor, "worldgen"); ++ // Paper - rewrite chunk system + + Objects.requireNonNull(mainThreadExecutor); +- ProcessorHandle mailbox = ProcessorHandle.of("main", mainThreadExecutor::tell); ++ // Paper - rewrite chunk system + + this.progressListener = worldGenerationProgressListener; + this.chunkStatusListener = chunkStatusChangeListener; +- ProcessorMailbox threadedmailbox1 = ProcessorMailbox.create(executor, "light"); ++ // Paper - rewrite chunk system + +- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE); +- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false); +- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false); +- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false)); ++ // Paper - rewrite chunk system ++ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system + this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); + this.overworldDataStorage = persistentStateManagerFactory; + this.poiManager = new PoiManager(path.resolve("poi"), dataFixer, dsync, world.registryAccess(), world); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Nullable + public ChunkHolder getUpdatingChunkIfPresent(long pos) { +- return (ChunkHolder) this.updatingChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkHolder getVisibleChunkIfPresent(long pos) { +- return (ChunkHolder) this.visibleChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + protected IntSupplier getChunkQueueLevel(long pos) { +- return () -> { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); +- }; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public String getChunkDebugData(ChunkPos chunkPos) { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end + + private CompletableFuture, ChunkHolder.ChunkLoadingFailure>> getChunkRangeFuture(ChunkPos centerChunk, int margin, IntFunction distanceToStatus) { +- List>> list = new ArrayList(); +- List list1 = new ArrayList(); +- int j = centerChunk.x; +- int k = centerChunk.z; +- +- for (int l = -margin; l <= margin; ++l) { +- for (int i1 = -margin; i1 <= margin; ++i1) { +- int j1 = Math.max(Math.abs(i1), Math.abs(l)); +- final ChunkPos chunkcoordintpair1 = new ChunkPos(j + i1, k + l); +- long k1 = chunkcoordintpair1.toLong(); +- ChunkHolder playerchunk = this.getUpdatingChunkIfPresent(k1); +- +- if (playerchunk == null) { +- return CompletableFuture.completedFuture(Either.right(new ChunkHolder.ChunkLoadingFailure() { +- public String toString() { +- return "Unloaded " + chunkcoordintpair1; +- } +- })); +- } +- +- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(j1); +- CompletableFuture> completablefuture = playerchunk.getOrScheduleFuture(chunkstatus, this); +- +- list1.add(playerchunk); +- list.add(completablefuture); +- } +- } +- +- CompletableFuture>> completablefuture1 = Util.sequence(list); +- CompletableFuture, ChunkHolder.ChunkLoadingFailure>> completablefuture2 = completablefuture1.thenApply((list2) -> { +- List list3 = Lists.newArrayList(); +- // CraftBukkit start - decompile error +- int cnt = 0; +- +- for (Iterator iterator = list2.iterator(); iterator.hasNext(); ++cnt) { +- final int l1 = cnt; +- // CraftBukkit end +- final Either either = (Either) iterator.next(); +- +- if (either == null) { +- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); +- } +- +- Optional optional = either.left(); +- +- if (!optional.isPresent()) { +- return Either.right(new ChunkHolder.ChunkLoadingFailure() { +- public String toString() { +- ChunkPos chunkcoordintpair2 = new ChunkPos(j + l1 % (margin * 2 + 1), k + l1 / (margin * 2 + 1)); +- +- return "Unloaded " + chunkcoordintpair2 + " " + either.right().get(); +- } +- }); +- } +- +- list3.add((ChunkAccess) optional.get()); +- } +- +- return Either.left(list3); +- }); +- Iterator iterator = list1.iterator(); +- +- while (iterator.hasNext()) { +- ChunkHolder playerchunk1 = (ChunkHolder) iterator.next(); +- +- playerchunk1.addSaveDependency("getChunkRangeFuture " + centerChunk + " " + margin, completablefuture2); +- } +- +- return completablefuture2; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public CompletableFuture> prepareEntityTickingChunk(ChunkPos pos) { +- return this.getChunkRangeFuture(pos, 2, (i) -> { +- return ChunkStatus.FULL; +- }).thenApplyAsync((either) -> { +- return either.mapLeft((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, this.mainThreadExecutor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { +- if (k > ChunkMap.MAX_CHUNK_DISTANCE && level > ChunkMap.MAX_CHUNK_DISTANCE) { +- return holder; +- } else { +- if (holder != null) { +- holder.setTicketLevel(level); +- } +- +- if (holder != null) { +- if (level > ChunkMap.MAX_CHUNK_DISTANCE) { +- this.toDrop.add(pos); +- } else { +- this.toDrop.remove(pos); +- } +- } +- +- if (level <= ChunkMap.MAX_CHUNK_DISTANCE && holder == null) { +- holder = (ChunkHolder) this.pendingUnloads.remove(pos); +- if (holder != null) { +- holder.setTicketLevel(level); +- } else { +- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); +- // Paper start +- net.minecraft.server.ChunkSystem.onChunkHolderCreate(this.level, holder); +- // Paper end +- } +- +- // Paper start +- holder.onChunkAdd(); +- // Paper end +- this.updatingChunkMap.put(pos, holder); +- this.modified = true; +- } +- +- return holder; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void close() throws IOException { +- try { +- this.queueSorter.close(); +- this.poiManager.close(); +- } finally { +- super.close(); +- } ++ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system ++ } + ++ // Paper start - rewrite chunk system ++ protected void saveIncrementally() { ++ this.level.chunkTaskScheduler.chunkHolderManager.autoSave(); // Paper - rewrite chunk system + } ++ // Paper end - - rewrite chunk system + + protected void saveAllChunks(boolean flush) { +- 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(); +- +- do { +- mutableboolean.setFalse(); +- list.stream().map((playerchunk) -> { +- CompletableFuture completablefuture; +- +- do { +- completablefuture = playerchunk.getChunkToSave(); +- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor; +- +- Objects.requireNonNull(completablefuture); +- iasynctaskhandler.managedBlock(completablefuture::isDone); +- } while (completablefuture != playerchunk.getChunkToSave()); +- +- return (ChunkAccess) completablefuture.join(); +- }).filter((ichunkaccess) -> { +- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk; +- }).filter(this::save).forEach((ichunkaccess) -> { +- mutableboolean.setTrue(); +- }); +- } while (mutableboolean.isTrue()); +- +- this.processUnloads(() -> { +- return true; +- }); +- this.flushWorker(); +- } else { +- net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); +- } +- ++ this.level.chunkTaskScheduler.chunkHolderManager.saveAllChunks(flush, false, false); // Paper - rewrite chunk system + } + + 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(); + } + + public boolean hasWork() { +- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || net.minecraft.server.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + 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()) { +- long j = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); +- +- if (playerchunk != null) { +- playerchunk.onChunkRemove(); // Paper +- this.pendingUnloads.put(j, playerchunk); +- this.modified = true; +- ++i; +- this.scheduleUnload(j, playerchunk); +- } +- } +- +- int k = Math.max(0, this.unloadQueue.size() - 2000); +- +- Runnable runnable; +- +- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { +- --k; +- runnable.run(); +- } +- +- int l = 0; +- Iterator objectiterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { +- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { +- ++l; +- } +- } ++ this.level.chunkTaskScheduler.chunkHolderManager.processUnloads(); // Paper - rewrite chunk system + + } + + private void scheduleUnload(long pos, ChunkHolder holder) { +- CompletableFuture completablefuture = holder.getChunkToSave(); +- Consumer consumer = (ichunkaccess) -> { // CraftBukkit - decompile error +- CompletableFuture completablefuture1 = holder.getChunkToSave(); +- +- if (completablefuture1 != completablefuture) { +- this.scheduleUnload(pos, holder); +- } else { +- // Paper start +- boolean removed; +- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { +- net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder); +- // Paper end +- if (ichunkaccess instanceof LevelChunk) { +- ((LevelChunk) ichunkaccess).setLoaded(false); +- } +- +- this.save(ichunkaccess); +- if (this.entitiesInLevel.remove(pos) && ichunkaccess instanceof LevelChunk) { +- LevelChunk chunk = (LevelChunk) ichunkaccess; +- +- this.level.unload(chunk); +- } +- +- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); +- this.lightEngine.tryScheduleUpdate(); +- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); +- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); +- } else if (removed) { // Paper start +- net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder); +- } // Paper end +- +- } +- }; +- Queue queue = this.unloadQueue; +- +- Objects.requireNonNull(this.unloadQueue); +- completablefuture.thenAcceptAsync(consumer, queue::add).whenComplete((ovoid, throwable) -> { +- if (throwable != null) { +- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable); +- } +- +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean promoteChunkMap() { +- if (!this.modified) { +- return false; +- } else { +- this.visibleChunkMap = this.updatingChunkMap.clone(); +- this.modified = false; +- return true; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> schedule(ChunkHolder holder, ChunkStatus requiredStatus) { +- ChunkPos chunkcoordintpair = holder.getPos(); +- +- if (requiredStatus == ChunkStatus.EMPTY) { +- return this.scheduleChunkLoad(chunkcoordintpair); +- } else { +- if (requiredStatus == ChunkStatus.LIGHT) { +- this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair); +- } +- +- Optional optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); +- +- if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) { +- CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (ichunkaccess) -> { +- return this.protoChunkToFullChunk(holder); +- }, (ChunkAccess) optional.get()); +- +- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); +- return completablefuture; +- } else { +- return this.scheduleChunkGeneration(holder, requiredStatus); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private CompletableFuture> scheduleChunkLoad(ChunkPos pos) { +- return this.readChunk(pos).thenApply((optional) -> { +- return optional.filter((nbttagcompound) -> { +- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound); +- +- if (!flag) { +- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); +- } +- +- return flag; +- }); +- }).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); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + +- 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 + } + + private CompletableFuture> scheduleChunkGeneration(ChunkHolder holder, ChunkStatus requiredStatus) { +- ChunkPos chunkcoordintpair = holder.getPos(); +- CompletableFuture, ChunkHolder.ChunkLoadingFailure>> completablefuture = this.getChunkRangeFuture(chunkcoordintpair, requiredStatus.getRange(), (i) -> { +- return this.getDependencyStatus(requiredStatus, i); +- }); +- +- this.level.getProfiler().incrementCounter(() -> { +- return "chunkGenerate " + requiredStatus.getName(); +- }); +- Executor executor = (runnable) -> { +- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }; +- +- return completablefuture.thenComposeAsync((either) -> { +- return (CompletionStage) either.map((list) -> { +- try { +- CompletableFuture> completablefuture1 = requiredStatus.generate(executor, this.level, this.generator, this.structureTemplateManager, this.lightEngine, (ichunkaccess) -> { +- return this.protoChunkToFullChunk(holder); +- }, list, false); +- +- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); +- return completablefuture1; +- } catch (Exception exception) { +- exception.getStackTrace(); +- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated"); +- +- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Generator", (Object) this.generator); +- this.mainThreadExecutor.execute(() -> { +- throw new ReportedException(crashreport); +- }); +- throw new ReportedException(crashreport); +- } +- }, (playerchunk_failure) -> { +- this.releaseLightTicket(chunkcoordintpair); +- return CompletableFuture.completedFuture(Either.right(playerchunk_failure)); +- }); +- }, executor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected void releaseLightTicket(ChunkPos pos) { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + })); + } + +- private ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { ++ public static ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { // Paper -> public, static + ChunkStatus chunkstatus1; + + if (distance == 0) { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + return chunkstatus1; + } + +- private static void postLoadProtoChunk(ServerLevel world, List nbt) { ++ public static void postLoadProtoChunk(ServerLevel world, List nbt) { // Paper - public + if (!nbt.isEmpty()) { + // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities + world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world).filter((entity) -> { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private CompletableFuture> protoChunkToFullChunk(ChunkHolder chunkHolder) { +- CompletableFuture> completablefuture = chunkHolder.getFutureIfPresentUnchecked(ChunkStatus.FULL.getParent()); +- +- return completablefuture.thenApplyAsync((either) -> { +- ChunkStatus chunkstatus = ChunkHolder.getStatus(chunkHolder.getTicketLevel()); +- +- return !chunkstatus.isOrAfter(ChunkStatus.FULL) ? ChunkHolder.UNLOADED_CHUNK : either.mapLeft((ichunkaccess) -> { +- try (Timing ignored = level.timings.chunkPostLoad.startTimingIfSync()) { // Paper +- ChunkPos chunkcoordintpair = chunkHolder.getPos(); +- ProtoChunk protochunk = (ProtoChunk) ichunkaccess; +- LevelChunk chunk; +- +- if (protochunk instanceof ImposterProtoChunk) { +- chunk = ((ImposterProtoChunk) protochunk).getWrapped(); +- } else { +- chunk = new LevelChunk(this.level, protochunk, (chunk1) -> { +- ChunkMap.postLoadProtoChunk(this.level, protochunk.getEntities()); +- }); +- chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); +- } +- +- chunk.setFullStatus(() -> { +- return ChunkHolder.getFullChunkStatus(chunkHolder.getTicketLevel()); +- }); +- chunk.runPostLoad(); +- if (this.entitiesInLevel.add(chunkcoordintpair.toLong())) { +- chunk.setLoaded(true); +- chunk.registerAllBlockEntitiesAfterLevelLoad(); +- chunk.registerTickContainerInLevel(this.level); +- } +- +- return chunk; +- } // Paper +- }); +- }, (runnable) -> { +- ProcessorHandle mailbox = this.mainThreadMailbox; +- long i = chunkHolder.getPos().toLong(); +- +- Objects.requireNonNull(chunkHolder); +- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, chunkHolder::getTicketLevel)); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> prepareTickingChunk(ChunkHolder holder) { +- ChunkPos chunkcoordintpair = holder.getPos(); +- CompletableFuture, ChunkHolder.ChunkLoadingFailure>> completablefuture = this.getChunkRangeFuture(chunkcoordintpair, 1, (i) -> { +- return ChunkStatus.FULL; +- }); +- 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) -> { +- chunk.postProcessGeneration(); +- this.level.startTickingChunk(chunk); +- }); +- }, this.mainThreadExecutor); +- +- completablefuture1.thenAcceptAsync((either) -> { +- either.ifLeft((chunk) -> { +- this.tickingGenerated.getAndIncrement(); +- MutableObject mutableobject = new MutableObject(); +- +- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> { +- this.playerLoadedChunk(entityplayer, mutableobject, chunk); +- }); +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }); +- return completablefuture1; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> prepareAccessibleChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder.getPos(), 1, ChunkStatus::getStatusAroundFullChunk).thenApplyAsync((either) -> { +- return either.mapLeft((list) -> { +- LevelChunk chunk = (LevelChunk) list.get(list.size() / 2); +- +- return chunk; +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public int getTickingGenerated() { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) { +- if (!chunkHolder.wasAccessibleSinceLastSave()) { +- return false; +- } else { +- ChunkAccess ichunkaccess = (ChunkAccess) chunkHolder.getChunkToSave().getNow(null); // CraftBukkit - decompile error +- +- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { +- return false; +- } else { +- long i = ichunkaccess.getPos().toLong(); +- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L); +- long k = System.currentTimeMillis(); +- +- if (k < j) { +- return false; +- } else { +- boolean flag = this.save(ichunkaccess); +- +- chunkHolder.refreshAccessibility(); +- if (flag) { +- this.chunkSaveCooldowns.put(i, k + 10000L); +- } +- +- return flag; +- } +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean save(ChunkAccess chunk) { +- this.poiManager.flush(chunk.getPos()); +- if (!chunk.isUnsaved()) { +- return false; +- } else { +- chunk.setUnsaved(false); +- ChunkPos chunkcoordintpair = chunk.getPos(); +- +- try { +- ChunkStatus chunkstatus = chunk.getStatus(); +- +- if (chunkstatus.getChunkType() != ChunkStatus.ChunkType.LEVELCHUNK) { +- if (this.isExistingChunkFull(chunkcoordintpair)) { +- return false; +- } +- +- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { +- return false; +- } +- } +- +- this.level.getProfiler().incrementCounter("chunkSave"); +- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk); +- +- this.write(chunkcoordintpair, nbttagcompound); +- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType()); +- return true; +- } catch (Exception exception) { +- ChunkMap.LOGGER.error("Failed to save chunk {},{}", new Object[]{chunkcoordintpair.x, chunkcoordintpair.z, exception}); +- return false; +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private boolean isExistingChunkFull(ChunkPos pos) { +- byte b0 = this.chunkTypeCache.get(pos.toLong()); +- +- if (b0 != 0) { +- return b0 == 1; +- } else { +- CompoundTag nbttagcompound; +- +- try { +- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null); +- if (nbttagcompound == null) { +- this.markPositionReplaceable(pos); +- return false; +- } +- } catch (Exception exception) { +- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception); +- this.markPositionReplaceable(pos); +- return false; +- } +- +- ChunkStatus.ChunkType chunkstatus_type = ChunkSerializer.getChunkTypeFromTag(nbttagcompound); +- +- return this.markPosition(pos, chunkstatus_type) == 1; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + ++ // Paper start - replace player loader system ++ public void setTickViewDistance(int distance) { ++ this.playerChunkManager.setTickDistance(distance); ++ } ++ // Paper end - replace player loader system + public void setViewDistance(int watchDistance) { + int j = Mth.clamp(watchDistance + 1, (int) 3, (int) 33); + +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int k = this.viewDistance; + + this.viewDistance = j; +- this.distanceManager.updatePlayerTickets(this.viewDistance + 1); +- Iterator objectiterator = net.minecraft.server.ChunkSystem.getUpdatingChunkHolders(this.level).iterator(); // Paper +- +- while (objectiterator.hasNext()) { +- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next(); +- ChunkPos chunkcoordintpair = playerchunk.getPos(); +- MutableObject mutableobject = new MutableObject(); +- +- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> { +- SectionPos sectionposition = entityplayer.getLastSectionPos(); +- boolean flag = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), k); +- boolean flag1 = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), this.viewDistance); +- +- this.updateChunkTracking(entityplayer, chunkcoordintpair, mutableobject, flag, flag1); +- }); +- } ++ this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system + } + + } + +- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { ++ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public + if (player.level == this.level) { + if (newWithinViewDistance && !oldWithinViewDistance) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong()); + + if (playerchunk != null) { +- LevelChunk chunk = playerchunk.getTickingChunk(); ++ LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system + + if (chunk != null) { + this.playerLoadedChunk(player, packet, chunk); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + void dumpChunks(Writer writer) throws IOException { +- CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); +- TickingTracker tickingtracker = this.distanceManager.tickingTracker(); +- Iterator objectbidirectionaliterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (objectbidirectionaliterator.hasNext()) { +- ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper +- long i = playerchunk.pos.toLong(); // Paper +- ChunkPos chunkcoordintpair = new ChunkPos(i); +- // Paper +- Optional optional = Optional.ofNullable(playerchunk.getLastAvailable()); +- Optional optional1 = optional.flatMap((ichunkaccess) -> { +- return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); +- }); +- +- // CraftBukkit - decompile error +- csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> { +- return chunk.getBlockEntities().size(); +- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> { +- return chunk.getBlockTicks().count(); +- }).orElse(0), optional1.map((chunk) -> { +- return chunk.getFluidTicks().count(); +- }).orElse(0)); +- } +- ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private static String printFuture(CompletableFuture> future) { +@@ -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 { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this.level, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ // Paper end - rewrite chunk system ++ return super.readSync(chunkcoordintpair); ++ } ++ ++ @Override ++ public void write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this.level, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ return; ++ } ++ // Paper end - rewrite chunk system ++ 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 +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.removePlayerFromDistanceMaps(player); // Paper - distance maps + } + +- for (int k = i - this.viewDistance - 1; k <= i + this.viewDistance + 1; ++k) { +- for (int l = j - this.viewDistance - 1; l <= j + this.viewDistance + 1; ++l) { +- if (ChunkMap.isChunkInRange(k, l, i, j, this.viewDistance)) { +- ChunkPos chunkcoordintpair = new ChunkPos(k, l); +- +- this.updateChunkTracking(player, chunkcoordintpair, new MutableObject(), !added, added); +- } +- } +- } ++ // Paper - handled by player chunk loader + + } + +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + SectionPos sectionposition = SectionPos.of((EntityAccess) player); + + player.setLastSectionPos(sectionposition); +- player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); ++ //player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); // Paper - handled by player chunk loader + return sectionposition; + } + +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int k1; + int l1; + +- if (Math.abs(i1 - i) <= this.viewDistance * 2 && Math.abs(j1 - j) <= this.viewDistance * 2) { +- k1 = Math.min(i, i1) - this.viewDistance - 1; +- l1 = Math.min(j, j1) - this.viewDistance - 1; +- int i2 = Math.max(i, i1) + this.viewDistance + 1; +- int j2 = Math.max(j, j1) + this.viewDistance + 1; +- +- for (int k2 = k1; k2 <= i2; ++k2) { +- for (int l2 = l1; l2 <= j2; ++l2) { +- boolean flag3 = ChunkMap.isChunkInRange(k2, l2, i1, j1, this.viewDistance); +- boolean flag4 = ChunkMap.isChunkInRange(k2, l2, i, j, this.viewDistance); +- +- this.updateChunkTracking(player, new ChunkPos(k2, l2), new MutableObject(), flag3, flag4); +- } +- } +- } else { +- boolean flag5; +- boolean flag6; +- +- for (k1 = i1 - this.viewDistance - 1; k1 <= i1 + this.viewDistance + 1; ++k1) { +- for (l1 = j1 - this.viewDistance - 1; l1 <= j1 + this.viewDistance + 1; ++l1) { +- if (ChunkMap.isChunkInRange(k1, l1, i1, j1, this.viewDistance)) { +- flag5 = true; +- flag6 = false; +- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), true, false); +- } +- } +- } +- +- for (k1 = i - this.viewDistance - 1; k1 <= i + this.viewDistance + 1; ++k1) { +- for (l1 = j - this.viewDistance - 1; l1 <= j + this.viewDistance + 1; ++l1) { +- if (ChunkMap.isChunkInRange(k1, l1, i, j, this.viewDistance)) { +- flag5 = false; +- flag6 = true; +- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), false, true); +- } +- } +- } +- } ++ // Paper - replaced by PlayerChunkLoader + + this.updateMaps(player); // Paper - distance maps ++ this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately + + } + + @Override + public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { +- Set set = this.playerMap.getPlayers(chunkPos.toLong()); +- Builder builder = ImmutableList.builder(); +- Iterator iterator = set.iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- SectionPos sectionposition = entityplayer.getLastSectionPos(); ++ // Paper start - per player view distance ++ // there can be potential desync with player's last mapped section and the view distance map, so use the ++ // view distance map here. ++ List ret = new java.util.ArrayList<>(4); ++ ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos); ++ if (players == null) { ++ return ret; ++ } + +- if (onlyOnWatchDistanceEdge && ChunkMap.isChunkOnRangeBorder(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance) || !onlyOnWatchDistanceEdge && ChunkMap.isChunkInRange(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance)) { +- builder.add(entityplayer); ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof ServerPlayer)) { ++ continue; ++ } ++ ServerPlayer player = (ServerPlayer)temp; ++ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) { ++ continue; + } ++ ret.add(player); + } + +- return builder.build(); ++ return ret; ++ // Paper end - per player view distance + } + + public void addEntity(Entity entity) { +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Override + protected boolean isChunkToRemove(long pos) { +- return ChunkMap.this.toDrop.contains(pos); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot + if (player != this.entity) { + Vec3 vec3d = player.position().subtract(this.entity.position()); +- double d0 = (double) Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16); ++ double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance + double d1 = vec3d.x * vec3d.x + vec3d.z * vec3d.z; + double d2 = d0 * d0; + boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 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 @@ import org.slf4j.Logger; + + public abstract class DistanceManager { + ++ // Paper start - rewrite chunk system ++ public io.papermc.paper.chunk.system.scheduling.ChunkHolderManager getChunkHolderManager() { ++ return this.chunkMap.level.chunkTaskScheduler.chunkHolderManager; ++ } ++ // Paper end - rewrite chunk system ++ + static final Logger LOGGER = LogUtils.getLogger(); + private static final int ENTITY_TICKING_RANGE = 2; + static final int PLAYER_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL) - 2; +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + private static final int ENTITY_TICKING_LEVEL_THRESHOLD = 32; + private static final int BLOCK_TICKING_LEVEL_THRESHOLD = 33; + final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); +- public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); +- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); ++ // Paper - rewrite chunk system + 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(); +- final ChunkTaskPriorityQueueSorter ticketThrottler; +- final ProcessorHandle> ticketThrottlerInput; +- final ProcessorHandle ticketThrottlerReleaser; +- final LongSet ticketsToRelease = new LongOpenHashSet(); +- final Executor mainThreadExecutor; +- private long ticketTickCounter; +- private int simulationDistance = 10; ++ //private final TickingTracker tickingTicketsTracker = new TickingTracker(); // Paper - no longer used ++ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used ++ // Paper - rewrite chunk system + private final ChunkMap chunkMap; // Paper + + protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { +- Objects.requireNonNull(mainThreadExecutor); +- ProcessorHandle mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); +- ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); +- +- this.ticketThrottler = chunktaskqueuesorter; +- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); +- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); +- this.mainThreadExecutor = mainThreadExecutor; ++ // Paper - rewrite chunk system + this.chunkMap = chunkMap; // Paper + } + + protected void purgeStaleTickets() { +- ++this.ticketTickCounter; +- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- if (ticket.timedOut(this.ticketTickCounter)) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } +- ++ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system + } + + private static int getTicketLevelAt(SortedArraySet> tickets) { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + + public boolean runAllUpdates(ChunkMap chunkStorage) { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- this.tickingTicketsTracker.runAllUpdates(); +- this.playerTicketManager.runAllUpdates(); +- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); +- boolean flag = i != 0; +- +- if (flag) { +- ; +- } +- +- 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 +- +- return true; +- } else { +- if (!this.ticketsToRelease.isEmpty()) { +- LongIterator longiterator = this.ticketsToRelease.iterator(); +- +- while (longiterator.hasNext()) { +- long j = longiterator.nextLong(); +- +- if (this.getTickets(j).stream().anyMatch((ticket) -> { +- return ticket.getType() == TicketType.PLAYER; +- })) { +- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j); +- +- if (playerchunk == null) { +- throw new IllegalStateException(); +- } +- +- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); +- +- completablefuture.thenAccept((either) -> { +- this.mainThreadExecutor.execute(() -> { +- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { +- }, j, false)); +- }); +- }); +- } +- } +- +- this.ticketsToRelease.clear(); +- } +- +- return flag; +- } ++ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system + } + + boolean addTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean +- SortedArraySet> arraysetsorted = this.getTickets(i); +- int j = DistanceManager.getTicketLevelAt(arraysetsorted); +- Ticket ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); +- +- ticket1.setCreatedTick(this.ticketTickCounter); +- if (ticket.getTicketLevel() < j) { +- this.ticketTracker.update(i, ticket.getTicketLevel(), true); +- } +- +- return ticket == ticket1; // CraftBukkit ++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper ++ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + boolean removeTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean +- SortedArraySet> arraysetsorted = this.getTickets(i); +- +- boolean removed = false; // CraftBukkit +- if (arraysetsorted.remove(ticket)) { +- removed = true; // CraftBukkit +- } +- +- if (arraysetsorted.isEmpty()) { +- this.tickets.remove(i); +- } +- +- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); +- return removed; // CraftBukkit ++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper ++ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + public void addTicket(TicketType type, ChunkPos pos, int level, T argument) { +- this.addTicket(pos.toLong(), new Ticket<>(type, level, argument)); ++ this.getChunkHolderManager().addTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system + } + + public void removeTicket(TicketType type, ChunkPos pos, int level, T argument) { +- Ticket ticket = new Ticket<>(type, level, argument); +- +- this.removeTicket(pos.toLong(), ticket); ++ this.getChunkHolderManager().removeTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system + } + + public void addRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + } + + public boolean addRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket ticket = new Ticket<>(tickettype, 33 - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean added = this.addTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.addTicket(j, ticket); +- return added; // CraftBukkit ++ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, 33 - i, t0); // Paper - rewrite chunk system + } + + public void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + } + + public boolean removeRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket ticket = new Ticket<>(tickettype, 33 - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean removed = this.removeTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.removeTicket(j, ticket); +- return removed; // CraftBukkit ++ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, 33 - i, t0); // Paper - rewrite chunk system + } + +- private SortedArraySet> getTickets(long position) { +- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> { +- return SortedArraySet.create(4); +- }); +- } ++ // Paper - rewrite chunk system + + protected void updateChunkForced(ChunkPos pos, boolean forced) { +- Ticket ticket = new Ticket<>(TicketType.FORCED, 31, pos); ++ Ticket ticket = new Ticket<>(TicketType.FORCED, 31, pos, 0L); // Paper - rewrite chunk system + long i = pos.toLong(); + + if (forced) { + this.addTicket(i, ticket); +- this.tickingTicketsTracker.addTicket(i, ticket); ++ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used + } else { + this.removeTicket(i, ticket); +- this.tickingTicketsTracker.removeTicket(i, ticket); ++ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used + } + + } +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + ChunkPos chunkcoordintpair = pos.chunk(); + long i = chunkcoordintpair.toLong(); + +- ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> { +- return new ObjectOpenHashSet(); +- })).add(player); ++ // Paper - no longer used + this.naturalSpawnChunkCounter.update(i, 0, true); +- this.playerTicketManager.update(i, 0, true); +- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used ++ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } + + public void removePlayer(SectionPos pos, ServerPlayer player) { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + if (objectset.isEmpty()) { + this.playersPerChunk.remove(i); + this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); +- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); +- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used ++ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } + + } + +- private int getPlayerTicketLevel() { +- return Math.max(0, 31 - this.simulationDistance); +- } ++ // Paper - rewrite chunk system + + public boolean inEntityTickingRange(long chunkPos) { +- return this.tickingTicketsTracker.getLevel(chunkPos) < 32; ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isEntityTickingReady(); ++ // Paper end - replace player chunk loader system + } + + public boolean inBlockTickingRange(long chunkPos) { +- return this.tickingTicketsTracker.getLevel(chunkPos) < 33; ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + protected String getTicketDebugString(long pos) { +- SortedArraySet> arraysetsorted = (SortedArraySet) this.tickets.get(pos); +- +- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket"; ++ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system + } + + protected void updatePlayerTickets(int viewDistance) { +- this.playerTicketManager.updateViewDistance(viewDistance); ++ this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager + } + + // Paper start + public int getSimulationDistance() { +- return this.simulationDistance; ++ return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager + } + // Paper end + + public void updateSimulationDistance(int simulationDistance) { +- if (simulationDistance != this.simulationDistance) { +- this.simulationDistance = simulationDistance; +- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); +- } +- ++ this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager + } + + public int getNaturalSpawnChunkCount() { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + } + + public String getDebugStatus() { +- return this.ticketThrottler.getDebugStatus(); ++ return "No DistanceManager stats available"; // Paper - rewrite chunk system + } + +- private void dumpTickets(String path) { +- try { +- FileOutputStream fileoutputstream = new FileOutputStream(new File(path)); +- +- try { +- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey()); +- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator(); +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + ticket.getType() + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)); +- } +- } +- } catch (Throwable throwable) { +- try { +- fileoutputstream.close(); +- } catch (Throwable throwable1) { +- throwable.addSuppressed(throwable1); +- } ++ // Paper - rewrite chunk system + +- throw throwable; +- } +- +- fileoutputstream.close(); +- } catch (IOException ioexception) { +- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception); +- } +- +- } +- +- @VisibleForTesting +- TickingTracker tickingTracker() { +- return this.tickingTicketsTracker; +- } ++ // Paper - replace player chunk loader + + public void removeTicketsOnClosing() { +- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve +- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- if (!immutableset.contains(ticket.getType())) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } +- ++ // Paper - rewrite chunk system - this stupid hack ain't needed anymore + } + + public boolean hasTickets() { +- return !this.tickets.isEmpty(); ++ return this.getChunkHolderManager().hasTickets(); // Paper - rewrite chunk system + } + + // CraftBukkit start + public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { +- Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); +- +- for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { +- Entry>> entry = iterator.next(); +- SortedArraySet> tickets = entry.getValue(); +- if (tickets.remove(target)) { +- // copied from removeTicket +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); +- +- // can't use entry after it's removed +- if (tickets.isEmpty()) { +- iterator.remove(); +- } +- } +- } ++ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system + } + // CraftBukkit end + ++ /* Paper - rewrite chunk system + private class ChunkTicketTracker extends ChunkTracker { + + public ChunkTicketTracker() { +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + return this.runUpdates(distance); + } + } ++ */ // Paper - rewrite chunk system + + private class FixedPlayerDistanceChunkTracker extends ChunkTracker { + +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + } + } + ++ /* Paper - rewrite chunk system + private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { + + private int viewDistance = 0; +@@ -0,0 +0,0 @@ public abstract class DistanceManager { + return distance <= this.viewDistance - 2; + } + } ++ */ // Paper - rewrite chunk system + } +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 LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { + long k = ChunkPos.asLong(x, z); + +- if (Thread.currentThread() == this.mainThread) { ++ if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return this.getChunkAtIfLoadedMainThread(x, z); + } + +@@ -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) { +- if (Thread.currentThread() != this.mainThread) { ++ final int x1 = x; final int z1 = z; // Paper - conflict on variable change ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return (ChunkAccess) CompletableFuture.supplyAsync(() -> { + return this.getChunk(x, z, leastStatus, create); + }, this.mainThreadProcessor).join(); +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + + ChunkAccess ichunkaccess; + +- for (int l = 0; l < 4; ++l) { +- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) { +- ichunkaccess = this.lastChunk[l]; +- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime +- return ichunkaccess; +- } +- } +- } ++ // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system + + 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 ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system ++ // Paper end + this.level.timings.syncChunkLoad.startTiming(); // Paper + chunkproviderserver_b.managedBlock(completablefuture::isDone); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - async chunk debug // Paper - rewrite chunk system + this.level.timings.syncChunkLoad.stopTiming(); // Paper + } // Paper + ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @Override + public LevelChunk getChunkNow(int chunkX, int chunkZ) { +- if (Thread.currentThread() != this.mainThread) { ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return null; + } else { + this.level.getProfiler().incrementCounter("getChunkNow"); +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + } + + public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- boolean flag1 = Thread.currentThread() == this.mainThread; ++ boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + CompletableFuture completablefuture; + + if (flag1) { +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + } + + private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ); +- long k = chunkcoordintpair.toLong(); +- int l = 33 + ChunkStatus.getDistance(leastStatus); +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); ++ // 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 start - rewrite chunk system ++ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); ++ int minLevel = 33 + ChunkStatus.getDistance(leastStatus); ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + +- // CraftBukkit start - don't add new ticket for currently unloading chunk +- boolean currentlyUnloading = false; +- if (playerchunk != null) { +- ChunkHolder.FullChunkStatus oldChunkState = ChunkHolder.getFullChunkStatus(playerchunk.oldTicketLevel); +- ChunkHolder.FullChunkStatus currentChunkState = ChunkHolder.getFullChunkStatus(playerchunk.getTicketLevel()); +- currentlyUnloading = (oldChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !currentChunkState.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); +- } +- if (create && !currentlyUnloading) { +- // CraftBukkit end +- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); +- if (this.chunkAbsent(playerchunk, l)) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); +- +- gameprofilerfiller.push("chunkLoad"); +- this.runDistanceManagerUpdates(); +- playerchunk = this.getVisibleChunkIfPresent(k); +- gameprofilerfiller.pop(); +- if (this.chunkAbsent(playerchunk, l)) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); +- } +- } ++ boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)); ++ ++ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { ++ return ChunkHolder.UNLOADED_CHUNK_FUTURE; + } + +- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); +- } ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion(); ++ if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(leastStatus)) { ++ // schedule ++ CompletableFuture> ret = new CompletableFuture<>(); ++ Consumer complete = (ChunkAccess chunk) -> { ++ if (chunk == null) { ++ ret.complete(Either.right(ChunkHolder.ChunkLoadingFailure.UNLOADED)); ++ } else { ++ ret.complete(Either.left(chunk)); ++ } ++ }; + +- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { +- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks ++ this.level.chunkTaskScheduler.scheduleChunkLoad( ++ chunkX, chunkZ, leastStatus, true, ++ isUrgent ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ complete ++ ); ++ ++ return ret; ++ } else { ++ // can return now ++ return CompletableFuture.completedFuture(Either.left(chunkCompletion.chunk())); ++ } ++ // Paper end - rewrite chunk system + } + ++ // Paper - rewrite chunk system ++ + @Override + public boolean hasChunk(int x, int z) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong()); +- int k = 33 + ChunkStatus.getDistance(ChunkStatus.FULL); +- +- return !this.chunkAbsent(playerchunk, k); ++ return this.getChunkAtIfLoadedImmediately(x, z) != null; // Paper - rewrite chunk system + } + + @Override +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + if (playerchunk == null) { + return null; + } else { +- int l = ServerChunkCache.CHUNK_STATUSES.size() - 1; +- +- while (true) { +- ChunkStatus chunkstatus = (ChunkStatus) ServerChunkCache.CHUNK_STATUSES.get(l); +- Optional optional = ((Either) playerchunk.getFutureIfPresentUnchecked(chunkstatus).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); +- +- if (optional.isPresent()) { +- return (BlockGetter) optional.get(); +- } +- +- if (chunkstatus == ChunkStatus.LIGHT.getParent()) { +- return null; +- } +- +- --l; ++ // Paper start - rewrite chunk system ++ ChunkStatus status = playerchunk.getChunkHolderStatus(); ++ if (status != null && !status.isOrAfter(ChunkStatus.LIGHT.getParent())) { ++ return null; + } ++ return playerchunk.getAvailableChunkNow(); ++ // Paper end - rewrite chunk system + } + } + +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + } + + public boolean runDistanceManagerUpdates() { +- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); +- boolean flag1 = this.chunkMap.promoteChunkMap(); +- +- if (!flag && !flag1) { +- return false; +- } else { +- this.clearCache(); +- return true; +- } ++ return this.level.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system + } + + // Paper start +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + // Paper end + + public boolean isPositionTicking(long pos) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- if (playerchunk == null) { +- return false; +- } else if (!this.level.shouldTickBlocksAt(pos)) { +- return false; +- } else { +- Either either = (Either) playerchunk.getTickingChunkFuture().getNow(null); // CraftBukkit - decompile error +- +- return either != null && either.left().isPresent(); +- } ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + public void save(boolean flush) { +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + this.close(true); + } + +- public void close(boolean save) throws IOException { +- if (save) { +- this.save(true); +- } +- // CraftBukkit end +- this.lightEngine.close(); +- this.chunkMap.close(); ++ public void close(boolean save) { // Paper - rewrite chunk system ++ this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system + } + + // 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(); +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().popPush("chunks"); + if (tickChunks) { + this.level.timings.chunks.startTiming(); // Paper - timings ++ this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes + this.tickChunks(); + this.level.timings.chunks.stopTiming(); // Paper - timings + } +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); + + if (playerchunk != null) { +- ((Either) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).left().ifPresent(chunkConsumer); ++ // Paper start - rewrite chunk system ++ LevelChunk chunk = playerchunk.getFullChunk(); ++ if (chunk != null) { ++ chunkConsumer.accept(chunk); ++ } ++ // Paper end - rewrite chunk system + } + + } +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { +- try { ++ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); + if (ServerChunkCache.this.runDistanceManagerUpdates()) { + return true; +- } else { +- ServerChunkCache.this.lightEngine.tryScheduleUpdate(); +- return super.pollTask(); + } +- } finally { +- chunkMap.callbackExecutor.run(); +- } +- // CraftBukkit end ++ return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // 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 { + private final MinecraftServer server; + public final PrimaryLevelData serverLevelData; // CraftBukkit - type + final EntityTickList entityTickList; +- public final PersistentEntitySectionManager entityManager; ++ //public final PersistentEntitySectionManager entityManager; // Paper - rewrite chunk system + public boolean noSave; + private final SleepStatus sleepStatus; + private int emptyTime; +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + } +- // Paper end ++ ++ // Paper start - rewrite chunk system ++ public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController chunkDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.getChunkSource().chunkMap.regionFileCache; ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.getChunkSource().chunkMap.readSync(new ChunkPos(chunkX, chunkZ)); ++ } ++ }; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController poiDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager(); ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.getChunkSource().chunkMap.getPoiManager().write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager().read(new ChunkPos(chunkX, chunkZ)); ++ } ++ }; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController entityDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.entityStorage; ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.writeEntityChunk(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.readEntityChunk(chunkX, chunkZ); ++ } ++ }; ++ private final EntityRegionFileStorage entityStorage; ++ ++ private static final class EntityRegionFileStorage extends net.minecraft.world.level.chunk.storage.RegionFileStorage { ++ ++ public EntityRegionFileStorage(Path directory, boolean dsync) { ++ super(directory, dsync); ++ } ++ ++ protected void write(ChunkPos pos, net.minecraft.nbt.CompoundTag nbt) throws IOException { ++ ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); ++ if (nbtPos != null && !pos.equals(nbtPos)) { ++ throw new IllegalArgumentException( ++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() ++ + " but compound says coordinate is " + nbtPos + " for world: " + this ++ ); ++ } ++ super.write(pos, nbt); ++ } ++ } ++ ++ private void writeEntityChunk(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this, chunkX, chunkZ, compound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ return; ++ } ++ this.entityStorage.write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ private net.minecraft.nbt.CompoundTag readEntityChunk(int chunkX, int chunkZ) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this, chunkX, chunkZ, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ return this.entityStorage.read(new ChunkPos(chunkX, chunkZ)); ++ } ++ ++ private final io.papermc.paper.chunk.system.entity.EntityLookup entityLookup; ++ public final io.papermc.paper.chunk.system.entity.EntityLookup getEntityLookup() { ++ return this.entityLookup; ++ } ++ // Paper end - rewrite chunk system + + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + // CraftBukkit end + boolean flag2 = minecraftserver.forceSynchronousWrites(); + DataFixer datafixer = minecraftserver.getFixerUpper(); +- EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); ++ this.entityStorage = new EntityRegionFileStorage(convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system //EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); + +- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); ++ // this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper // Paper - rewrite chunk system + StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); + int j = this.spigotConfig.viewDistance; // Spigot + int k = this.spigotConfig.simulationDistance; // Spigot +- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; ++ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system + +- Objects.requireNonNull(this.entityManager); +- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> { ++ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system ++ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system + return minecraftserver.overworld().getDataStorage(); + }); + chunkgenerator.ensureStructuresGenerated(this.chunkSource.randomState()); +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.sleepStatus = new SleepStatus(); + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ ++ this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system ++ this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system + } + + public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.push("checkDespawn"); + entity.checkDespawn(); + gameprofilerfiller.pop(); +- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { ++ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); + + if (entity1 != null) { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + gameprofilerfiller.push("entityManagement"); +- this.entityManager.tick(); ++ //this.entityManager.tick(); // Paper - rewrite chunk system + gameprofilerfiller.popPush("gameEvents"); + this.sendGameEvents(); + gameprofilerfiller.pop(); +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @Override + public boolean shouldTickBlocksAt(long chunkPos) { +- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos); ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + protected void tickTime() { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { ++ // Paper start - rewrite chunk system - add close param ++ this.save(progressListener, flush, savingDisabled, false); ++ } ++ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) { ++ // Paper end - rewrite chunk system - add close param + ServerChunkCache chunkproviderserver = this.getChunkSource(); + + if (!savingDisabled) { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + timings.worldSaveChunks.startTiming(); // Paper +- chunkproviderserver.save(flush); ++ if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system ++ if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system + timings.worldSaveChunks.stopTiming(); // Paper + }// Paper +- if (flush) { +- this.entityManager.saveAll(); +- } else { +- this.entityManager.autoSave(); +- } ++ // Paper - rewrite chunk system - entity saving moved into ChunkHolder + +- } ++ } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system + + // CraftBukkit start - moved from MinecraftServer.saveChunks + ServerLevel worldserver1 = this; +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED); + } + +- this.entityManager.addNewEntity(player); ++ this.entityLookup.addNewEntity(player); // Paper - rewite chunk system + } + + // CraftBukkit start +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // CraftBukkit end + +- return this.entityManager.addNewEntity(entity); ++ return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system + } + } + +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { + // CraftBukkit end + Stream stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error +- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; ++ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system + +- Objects.requireNonNull(this.entityManager); +- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) { ++ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system ++ if (stream.anyMatch(this.entityLookup::hasEntity)) { // Paper - rewrite chunk system + return false; + } else { + this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats())); ++ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system + bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); + bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); + bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1); + + try { +- playerchunkmap.dumpChunks(bufferedwriter2); ++ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system + } catch (Throwable throwable4) { + if (bufferedwriter2 != null) { + try { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2); + + try { +- this.entityManager.dumpSections(bufferedwriter3); ++ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system + } catch (Throwable throwable6) { + if (bufferedwriter3 != null) { + try { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @VisibleForTesting + public String getWatchdogStats() { +- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> { ++ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system + return Registry.ENTITY_TYPE.getKey(entity.getType()).toString(); + }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); + } +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + @Override + public LevelEntityGetter getEntities() { + org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot +- return this.entityManager.getEntityGetter(); ++ return this.entityLookup; // Paper - rewrite chunk system + } + + public void addLegacyChunkEntities(Stream entities) { +- this.entityManager.addLegacyChunkEntities(entities); ++ this.entityLookup.addLegacyChunkEntities(entities.toList()); // Paper - rewrite chunk system + } + + public void addWorldGenChunkEntities(Stream entities) { +- this.entityManager.addWorldGenChunkEntities(entities); ++ this.entityLookup.addWorldGenChunkEntities(entities.toList()); // Paper - rewrite chunk system + } + + public void startTickingChunk(LevelChunk chunk) { +@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel { + @Override + public void close() throws IOException { + super.close(); +- this.entityManager.close(); ++ //this.entityManager.close(); // Paper - rewrite chunk system + } + + @Override + public String gatherChunkSourceStats() { + String s = this.chunkSource.gatherStats(); + +- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats(); ++ return "Chunks[S] W: " + s + " E: " + this.entityLookup.getDebugInfo(); // Paper - rewrite chunk system + } + + public boolean areEntitiesLoaded(long chunkPos) { +- return this.entityManager.areEntitiesLoaded(chunkPos); ++ // Paper start - rewrite chunk system ++ return this.getChunkIfLoadedImmediately(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos)) != null; ++ // Paper end - rewrite chunk system + } + + private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { +- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos); ++ // Paper start - optimize is ticking ready type functions ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos); ++ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded ++ return chunkHolder != null && chunkHolder.isTickingReady(); ++ // Paper end + } + + public boolean isPositionEntityTicking(BlockPos pos) { +- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos)); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + public boolean isNaturalSpawningAllowed(BlockPos pos) { +- return this.entityManager.canPositionTick(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + public boolean isNaturalSpawningAllowed(ChunkPos pos) { +- return this.entityManager.canPositionTick(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + private final class EntityCallbacks implements LevelCallback { +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.ChunkStatus; + + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { + private static final Logger LOGGER = LogUtils.getLogger(); +- private final ProcessorMailbox taskMailbox; +- private final ObjectList> lightTasks = new ObjectArrayList<>(); ++ // Paper - rewrite chunk system + private final ChunkMap chunkMap; +- private final ProcessorHandle> sorterMailbox; ++ // Paper - rewrite chunk system + private volatile int taskPerBatch = 5; +- private final AtomicBoolean scheduled = new AtomicBoolean(); ++ // Paper - rewrite chunk system + + // Paper start - replace light engine impl +- protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; ++ public final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; + public final boolean hasBlockLight; + public final boolean hasSkyLight; + // Paper end - replace light engine impl +@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { + super(chunkProvider, false, false); // Paper - destroy vanilla light engine state + this.chunkMap = chunkStorage; +- this.sorterMailbox = executor; +- this.taskMailbox = processor; ++ // Paper - rewrite chunk system + // Paper start - replace light engine impl + this.hasBlockLight = true; + this.hasSkyLight = hasBlockLight; // Nice variable name. +@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + ++totalChunks; + } + +- this.taskMailbox.tell(() -> { ++ this.chunkMap.level.chunkTaskScheduler.lightExecutor.queueRunnable(() -> { // Paper - rewrite chunk system + this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { + chunkLightCallback.accept(chunkPos); + ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { +@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + for (int dz = -1; dz <= 1; ++dz) { + ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ)); + if (neighbour != null) { +- neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> { +- return curr; +- }); ++ // Paper - rewrite chunk system - not needed, light ticket will keep these chunks loaded + } + } + } +@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> { +- this.lightTasks.add(Pair.of(stage, task)); +- if (this.lightTasks.size() >= this.taskPerBatch) { +- this.runUpdate(); +- } +- +- }, ChunkPos.asLong(x, z), completedLevelSupplier)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override +@@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + }); + } +- // Paper end - replace light engine impl +- ChunkPos chunkPos = chunk.getPos(); +- chunk.setLightCorrect(false); +- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- LevelChunkSection[] levelChunkSections = chunk.getSections(); +- +- for(int i = 0; i < chunk.getSectionsCount(); ++i) { +- LevelChunkSection levelChunkSection = levelChunkSections[i]; +- if (!levelChunkSection.hasOnlyAir()) { +- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i); +- super.updateSectionStatus(SectionPos.of(chunkPos, j), false); +- } +- } +- +- super.enableLightSources(chunkPos, true); +- if (!excludeBlocks) { +- chunk.getLights().forEach((pos) -> { +- super.onBlockEmissionIncrease(pos, chunk.getLightEmission(pos)); +- }); +- } +- +- }, () -> { +- return "lightChunk " + chunkPos + " " + excludeBlocks; +- })); +- return CompletableFuture.supplyAsync(() -> { +- chunk.setLightCorrect(true); +- super.retainData(chunkPos, false); +- this.chunkMap.releaseLightTicket(chunkPos); +- return chunk; +- }, (runnable) -> { +- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable); +- }); ++ throw new InternalError(); // Paper - rewrite chunk system + } + + public void tryScheduleUpdate() { +- if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine +- this.taskMailbox.tell(() -> { +- this.runUpdate(); +- this.scheduled.set(false); +- }); +- } +- ++ // Paper - rewrite chunk system + } + + private void runUpdate() { +- int i = Math.min(this.lightTasks.size(), this.taskPerBatch); +- ObjectListIterator> objectListIterator = this.lightTasks.iterator(); +- +- int j; +- for(j = 0; objectListIterator.hasNext() && j < i; ++j) { +- Pair pair = objectListIterator.next(); +- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { +- pair.getSecond().run(); +- } +- } +- +- objectListIterator.back(j); +- this.theLightEngine.propagateChanges(); // Paper - rewrite light engine +- +- for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) { +- Pair pair2 = objectListIterator.next(); +- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { +- pair2.getSecond().run(); +- } +- +- objectListIterator.remove(); +- } +- ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void setTaskPerBatch(int taskBatchSize) { +diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java +index 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 TicketType type; + private final int ticketLevel; + public final T key; +- public long createdTick; ++ // Paper start - rewrite chunk system ++ public final long removalTick; + +- protected Ticket(TicketType type, int level, T argument) { ++ public Ticket(TicketType type, int level, T argument, long removalTick) { ++ this.removalTick = removalTick; ++ // Paper end - rewrite chunk system + this.type = type; + this.ticketLevel = level; + this.key = argument; +@@ -0,0 +0,0 @@ public final class Ticket implements Comparable> { + + @Override + public String toString() { +- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; ++ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die on " + this.removalTick; // Paper - rewrite chunk system + } + + public TicketType getType() { +@@ -0,0 +0,0 @@ public final class Ticket implements Comparable> { + } + + protected void setCreatedTick(long tickCreated) { +- this.createdTick = tickCreated; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean timedOut(long currentTick) { +- long l = this.type.timeout(); +- return l != 0L && currentTick - this.createdTick > l; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + } +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; +@@ -0,0 +0,0 @@ public class TicketType { + 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 CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting ++ // Paper start - rewrite chunk system ++ public static final TicketType CHUNK_LOAD = create("chunk_load", Long::compareTo); ++ public static final TicketType STATUS_UPGRADE = create("status_upgrade", Long::compareTo); ++ public static final TicketType ENTITY_LOAD = create("entity_load", Long::compareTo); ++ public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); ++ public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); ++ // Paper end - rewrite chunk system + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -0,0 +0,0 @@ public class WorldGenRegion implements WorldGenLevel { + public long nextSubTickCount() { + return this.subTickCount.getAndIncrement(); + } ++ ++ // Paper start ++ // No-op, this class doesn't provide entity access ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Paper end + } +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/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -0,0 +0,0 @@ public abstract class PlayerList { + boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); + + // Spigot - view distance +- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); ++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat(), player.getLastDeathLocation())); // Paper - replace old player chunk management + player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); + playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); +@@ -0,0 +0,0 @@ public abstract class PlayerList { + // CraftBukkit start + LevelData worlddata = worldserver1.getLevelData(); + entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionTypeId(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag, entityplayer1.getLastDeathLocation())); +- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot +- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot ++ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management ++ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management + entityplayer1.spawnIn(worldserver1); + entityplayer1.unsetRemoved(); + entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot())); +@@ -0,0 +0,0 @@ public abstract class PlayerList { + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; +- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); ++ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +@@ -0,0 +0,0 @@ public abstract class PlayerList { + + public void setSimulationDistance(int simulationDistance) { + this.simulationDistance = simulationDistance; +- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); ++ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/util/SortedArraySet.java ++++ b/src/main/java/net/minecraft/util/SortedArraySet.java +@@ -0,0 +0,0 @@ public class SortedArraySet extends AbstractSet { + this.contents = (T[])castRawArray(new Object[initialCapacity]); + } + } ++ // Paper start - optimise removeIf ++ @Override ++ public boolean removeIf(java.util.function.Predicate filter) { ++ // prev. impl used an iterator, which could be n^2 and creates garbage ++ int i = 0, len = this.size; ++ T[] backingArray = this.contents; ++ ++ for (;;) { ++ if (i >= len) { ++ return false; ++ } ++ if (!filter.test(backingArray[i])) { ++ ++i; ++ continue; ++ } ++ break; ++ } ++ ++ // we only want to write back to backingArray if we really need to ++ ++ int lastIndex = i; // this is where new elements are shifted to ++ ++ for (; i < len; ++i) { ++ T curr = backingArray[i]; ++ if (!filter.test(curr)) { // if test throws we're screwed ++ backingArray[lastIndex++] = curr; ++ } ++ } ++ ++ // cleanup end ++ Arrays.fill(backingArray, lastIndex, len, null); ++ this.size = lastIndex; ++ return true; ++ } ++ // Paper end - optimise removeIf + + public static > SortedArraySet create() { + return create(10); +@@ -0,0 +0,0 @@ public class SortedArraySet extends AbstractSet { + } + } + ++ // Paper start - rewrite chunk system ++ public T replace(T object) { ++ int i = this.findIndex(object); ++ if (i >= 0) { ++ T old = this.contents[i]; ++ this.contents[i] = object; ++ return old; ++ } else { ++ this.addInternal(object, getInsertionPosition(i)); ++ return object; ++ } ++ } ++ ++ public T removeAndGet(T object) { ++ int i = this.findIndex(object); ++ if (i >= 0) { ++ final T ret = this.contents[i]; ++ this.removeInternal(i); ++ return ret; ++ } else { ++ return null; ++ } ++ } ++ // Paper end - rewrite chunk system ++ + @Override + public boolean remove(Object object) { + int i = this.findIndex((T)object); +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/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + // Paper end + ++ // Paper start ++ /** ++ * Overriding this field will cause memory leaks. ++ */ ++ private final boolean hardCollides; ++ ++ private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); ++ { ++ /* // Goodbye, broken on reobf... ++ Boolean hardCollides = cachedOverrides.get(this.getClass()); ++ if (hardCollides == null) { ++ try { ++ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); ++ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); ++ if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) ++ || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { ++ hardCollides = Boolean.TRUE; ++ } else { ++ hardCollides = Boolean.FALSE; ++ } ++ cachedOverrides.put(this.getClass(), hardCollides); ++ } ++ catch (ThreadDeath thr) { throw thr; } ++ catch (Throwable thr) { ++ // shouldn't happen, just explode ++ throw new RuntimeException(thr); ++ } ++ } */ ++ this.hardCollides = this instanceof Boat ++ || this instanceof net.minecraft.world.entity.monster.Shulker ++ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart ++ || this.shouldHardCollide(); ++ } ++ ++ // plugins can override ++ protected boolean shouldHardCollide() { ++ return false; ++ } ++ ++ public final boolean hardCollides() { ++ return this.hardCollides; ++ } ++ ++ public net.minecraft.server.level.ChunkHolder.FullChunkStatus chunkStatus; ++ ++ public int sectionX = Integer.MIN_VALUE; ++ public int sectionY = Integer.MIN_VALUE; ++ public int sectionZ = Integer.MIN_VALUE; ++ ++ public boolean updatingSectionStatus = false; ++ // Paper end ++ + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); + this.passengers = ImmutableList.of(); +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return InteractionResult.PASS; + } + +- public boolean canCollideWith(Entity other) { ++ public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override + return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); + } + +- public boolean canBeCollidedWith() { ++ public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override + return false; + } + +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + }; + } + ++ // Paper start - rewrite chunk system ++ public boolean hasAnyPlayerPassengers() { ++ // copied from below ++ if (this.passengers.isEmpty()) { return false; } ++ return this.getIndirectPassengersStream().anyMatch((entity) -> { ++ return entity instanceof Player; ++ }); ++ } ++ // Paper end - rewrite chunk system ++ + public boolean hasExactlyOnePlayerPassenger() { + return this.getIndirectPassengersStream().filter((entity) -> { + return entity instanceof Player; +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + } + + public final void setPosRaw(double x, double y, double z) { ++ // 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()); ++ return; ++ } ++ // Paper end - rewrite chunk system + 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); +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + @Override + public final void setRemoved(Entity.RemovalReason reason) { ++ // Paper start - rewrite chunk system ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot remove entity off-main"); ++ if (this.updatingSectionStatus) { ++ LOGGER.warn("Entity " + this + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); ++ return; ++ } ++ // Paper end - rewrite chunk system + if (this.removalReason == null) { + this.removalReason = reason; + } +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.stopRiding(); + } + +- this.getPassengers().forEach(Entity::stopRiding); ++ if (reason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) + this.levelCallback.onRemove(reason); + } + +@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + @Override + public boolean shouldBeSaved() { +- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger()); ++ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasAnyPlayerPassengers()); // Paper - rewrite chunk system - it should check if the entity has ANY player passengers + } + + @Override +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 @@ 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; +- private final LongSet loadedChunks = new LongOpenHashSet(); ++ // Paper start - rewrite chunk system ++ // the vanilla tracker needs to be replaced because it does not support level removes ++ public final net.minecraft.server.level.ServerLevel world; ++ 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 - rewrite chunk system ++ + + 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.distanceTracker = new PoiManager.DistanceTracker(); ++ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system + } + + public void add(BlockPos pos, Holder 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) { +- super.tick(shouldKeepTicking); +- this.distanceTracker.runAllUpdates(); ++ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system + } + + @Override +- protected void setDirty(long pos) { +- super.setDirty(pos); +- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); ++ public void setDirty(long pos) { ++ // Paper start - rewrite chunk system ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ io.papermc.paper.chunk.system.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false); ++ if (chunk != null) { ++ chunk.setDirty(true); ++ } ++ this.updateDistanceTracking(pos); ++ // Paper end - rewrite chunk system + } + + @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 + } + ++ @Override ++ public Optional get(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ ++ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); ++ } ++ ++ @Override ++ public Optional getOrLoad(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ ++ if (chunkY >= io.papermc.paper.util.WorldUtil.getMinSection(this.world) && ++ chunkY <= io.papermc.paper.util.WorldUtil.getMaxSection(this.world)) { ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ if (ret != null) { ++ return ret.getSectionForVanilla(chunkY); ++ } else { ++ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); ++ } ++ } ++ // retain vanilla behavior: do not load section if out of bounds! ++ return Optional.empty(); ++ } ++ ++ @Override ++ protected PoiSection getOrCreate(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ if (ret != null) { ++ return ret.getOrCreateSection(chunkY); ++ } else { ++ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); ++ } ++ } ++ ++ public void onUnload(long coordinate) { // Paper - rewrite chunk system ++ int chunkX = net.minecraft.server.MCUtil.getCoordinateX(coordinate); ++ int chunkZ = net.minecraft.server.MCUtil.getCoordinateZ(coordinate); ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); ++ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) { ++ long sectionPos = SectionPos.asLong(chunkX, section, chunkZ); ++ this.updateDistanceTracking(sectionPos); ++ } ++ } ++ ++ public void loadInPoiChunk(io.papermc.paper.chunk.system.poi.PoiChunk poiChunk) { ++ int chunkX = poiChunk.chunkX; ++ int chunkZ = poiChunk.chunkZ; ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); ++ for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) { ++ PoiSection section = poiChunk.getSection(sectionY); ++ if (section != null && !section.isEmpty()) { ++ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); ++ } ++ } ++ } ++ // Paper end - rewrite chunk system ++ + public void checkConsistencyWithBlocks(ChunkPos chunkPos, LevelChunkSection chunkSection) { + SectionPos sectionPos = SectionPos.of(chunkPos, SectionPos.blockToSectionCoord(chunkSection.bottomBlockY())); + Util.ifElse(this.getOrLoad(sectionPos.asLong()), (poiSet) -> { +@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage { + }).map((pair) -> { + return pair.getFirst().chunk(); + }).filter((chunkPos) -> { +- return this.loadedChunks.add(chunkPos.toLong()); ++ return true; // Paper - rewrite chunk system + }).forEach((chunkPos) -> { + world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY); + }); +@@ -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 - rewrite chunk system - diff on change, this specifies the source level to use for distance tracking + } + + @Override +@@ -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 { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ // Paper end - rewrite chunk system ++ return super.read(chunkcoordintpair); ++ } ++ ++ @Override ++ public void write(ChunkPos chunkcoordintpair, net.minecraft.nbt.CompoundTag nbttagcompound) throws java.io.IOException { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA); ++ return; ++ } ++ // Paper end - rewrite chunk system ++ 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/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -0,0 +0,0 @@ public class PoiSection { + private final Map, Set> byType = Maps.newHashMap(); + private final Runnable setDirty; + private boolean isValid; ++ public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system + + public static Codec codec(Runnable updateListener) { + return RecordCodecBuilder.create((instance) -> { // Paper - decompile fix +@@ -0,0 +0,0 @@ public class PoiSection { + this(updateListener, true, ImmutableList.of()); + } + ++ // Paper start - isEmpty ++ public boolean isEmpty() { ++ return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); ++ } ++ // Paper end ++ + private PoiSection(Runnable updateListener, boolean valid, List pois) { + this.setDirty = updateListener; + this.isValid = valid; +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -0,0 +0,0 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + + public interface EntityGetter { ++ ++ // Paper start ++ List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); ++ ++ void getEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate); ++ // Paper end ++ + List getEntities(@Nullable Entity except, AABB box, Predicate predicate); + + List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); +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 { + + if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement + this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); ++ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance ++ // if copied from above ++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management ++ ((ServerLevel)this).getChunkSource().blockChanged(blockposition); ++ // Paper end - per player view distance + } + + if ((i & 1) != 0) { +@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return this.capturedTileEntities.get(blockposition); + } + // CraftBukkit end +- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); ++ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system + } + + public void setBlockEntity(BlockEntity blockEntity) { +@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); +- +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } +- +- if (entity1 instanceof EnderDragon) { +- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); +- int i = aentitycomplexpart.length; +- +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; +- +- if (entity1 != except && predicate.test(entitycomplexpart)) { +- list.add(entitycomplexpart); +- } +- } +- } +- +- }); ++ ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call + return list; + } + +@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +- this.getEntities().get(filter, box, (entity) -> { +- if (predicate.test(entity)) { +- list.add(entity); +- } +- +- if (entity instanceof EnderDragon) { +- EnderDragon entityenderdragon = (EnderDragon) entity; +- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities(); +- int i = aentitycomplexpart.length; +- +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; +- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error +- +- if (t0 != null && predicate.test(t0)) { +- list.add(t0); +- } +- } ++ // Paper start - optimise this call ++ if (filter instanceof net.minecraft.world.entity.EntityType) { ++ ((ServerLevel)this).getEntityLookup().getEntities((net.minecraft.world.entity.EntityType)filter, box, list, predicate); ++ } else { ++ Predicate test = (obj) -> { ++ return filter.tryCast(obj) != null; ++ }; ++ predicate = predicate == null ? test : test.and((Predicate)predicate); ++ Class base; ++ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { ++ ((ServerLevel)this).getEntityLookup().getEntities((Entity) null, box, (List)list, (Predicate)predicate); ++ } else { ++ ((ServerLevel)this).getEntityLookup().getEntities(base, null, box, (List)list, (Predicate)predicate); // Paper - optimise this call + } +- +- }); ++ } ++ // Paper end - optimise this call + return list; + } + +@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public long nextSubTickCount() { + return (long) (this.subTickCount++); + } ++ ++ // Paper start ++ //protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - rewrite chunk system ++ ++ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { ++ io.papermc.paper.world.ChunkEntitySlices slices = ((ServerLevel)this).getEntityLookup().getChunk(chunkX, chunkZ); ++ if (slices == null) { ++ return new org.bukkit.entity.Entity[0]; ++ } ++ return slices.getChunkEntities(); ++ } ++ ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, ret, predicate); ++ return ret; ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) { ++ ((ServerLevel)this).getEntityLookup().getEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { ++ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate) { ++ ((ServerLevel)this).getEntityLookup().getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); ++ } ++ ++ @Override ++ public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ ((ServerLevel)this).getEntityLookup().getEntities(entityClass, null, box, ret, predicate); ++ return ret; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -0,0 +0,0 @@ public abstract class ChunkGenerator { + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { + chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler()); + return chunk; +- }), Util.backgroundExecutor()); ++ }), executor); // Paper - run with supplied executor + } + + public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +@@ -0,0 +0,0 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp + + public class ChunkStatus { + ++ // Paper start - rewrite chunk system ++ public boolean isParallelCapable; // Paper ++ public int writeRadius = -1; ++ public int loadRange = 0; ++ ++ protected static final java.util.List statuses = new java.util.ArrayList<>(); ++ ++ private ChunkStatus nextStatus; ++ ++ public final ChunkStatus getNextStatus() { ++ return this.nextStatus; ++ } ++ ++ public final boolean isEmptyLoadStatus() { ++ return this.loadingTask == PASSTHROUGH_LOAD_TASK; ++ } ++ ++ public final boolean isEmptyGenStatus() { ++ return this == ChunkStatus.EMPTY || this == ChunkStatus.HEIGHTMAPS || this == ChunkStatus.LIQUID_CARVERS; ++ } ++ ++ public final java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean(); ++ // Paper end - rewrite chunk system ++ + public static final int MAX_STRUCTURE_DISTANCE = 8; + private static final EnumSet PRE_FEATURES = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); + public static final EnumSet POST_FEATURES = EnumSet.of(Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE, Heightmap.Types.MOTION_BLOCKING, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); +@@ -0,0 +0,0 @@ public class ChunkStatus { + protochunk.setStatus(chunkstatus); + } + +- return lightenginethreaded.retainData(ichunkaccess).thenApply(Either::left); +- }, (chunkstatus, worldserver, structuretemplatemanager, lightenginethreaded, function, ichunkaccess) -> { +- return lightenginethreaded.retainData(ichunkaccess).thenApply(Either::left); +- }); ++ return CompletableFuture.completedFuture(Either.left(ichunkaccess)); // Paper - rewrite chunk system ++ }); // Paper - rewrite chunk system + public static final ChunkStatus LIGHT = ChunkStatus.register("light", ChunkStatus.FEATURES, 1, ChunkStatus.POST_FEATURES, ChunkStatus.ChunkType.PROTOCHUNK, (chunkstatus, executor, worldserver, chunkgenerator, structuretemplatemanager, lightenginethreaded, function, list, ichunkaccess, flag) -> { + return ChunkStatus.lightChunk(chunkstatus, lightenginethreaded, ichunkaccess); + }, (chunkstatus, worldserver, structuretemplatemanager, lightenginethreaded, function, ichunkaccess) -> { +@@ -0,0 +0,0 @@ public class ChunkStatus { + this.chunkType = chunkType; + this.heightmapsAfter = heightMapTypes; + this.index = previous == null ? 0 : previous.getIndex() + 1; ++ // Paper start ++ this.nextStatus = this; ++ if (statuses.size() > 0) { ++ statuses.get(statuses.size() - 1).nextStatus = this; ++ } ++ statuses.add(this); ++ // Paper end + } + + public int getIndex() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + + protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { + ++ // Paper start - no-tick view distance ++ ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource(); ++ net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap; ++ // this code handles the addition of ticking tickets - the distance map handles the removal ++ if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { ++ if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system ++ // now we're ready for entity ticking ++ chunkProviderServer.mainThreadProcessor.execute(() -> { ++ // double check that this condition still holds. ++ if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system ++ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk ++ chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update ++ } ++ }); ++ } ++ } ++ ++ // this code handles the chunk sending ++ if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { ++ // Paper start - replace old player chunk loading system ++ if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) { ++ // the post processing is expensive, so we don't want to run it unless we're actually near ++ // a player. ++ chunkProviderServer.mainThreadProcessor.execute(() -> { ++ if (!LevelChunk.this.areNeighboursLoaded(1)) { ++ return; ++ } ++ LevelChunk.this.postProcessGeneration(); ++ if (!LevelChunk.this.areNeighboursLoaded(1)) { ++ return; ++ } ++ chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z); ++ }); ++ } ++ // Paper end - replace old player chunk loading system ++ } ++ // Paper end - no-tick view distance + } + + public final boolean isAnyNeighborsLoaded() { +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + + } + ++ // Paper start - new load callbacks ++ private io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder; ++ public io.papermc.paper.chunk.system.scheduling.NewChunkHolder getChunkHolder() { ++ return this.chunkHolder; ++ } ++ ++ public void setChunkHolder(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ if (chunkHolder == null) { ++ throw new NullPointerException("Chunkholder cannot be null"); ++ } ++ if (this.chunkHolder != null) { ++ throw new IllegalStateException("Already have chunkholder: " + this.chunkHolder + ", cannot replace with " + chunkHolder); ++ } ++ this.chunkHolder = chunkHolder; ++ this.playerChunk = chunkHolder.vanillaChunkHolder; ++ } ++ ++ /* Note: We skip the light neighbour chunk loading done for the vanilla full chunk */ ++ /* Starlight does not need these chunks for lighting purposes because of edge checks */ ++ ++ public void onChunkLoad(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ // figure out how this should interface with: ++ // the entity chunk load event // -> moved to the FULL status ++ // the chunk load event // -> stays here ++ // any entity add to world events // -> in FULL status ++ this.loadCallback(); ++ net.minecraft.server.ChunkSystem.onChunkBorder(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkUnload(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ // figure out how this should interface with: ++ // the entity chunk load event // -> moved to chunk unload to disk (not written yet) ++ // the chunk load event // -> stays here ++ // any entity add to world events // -> goes into the unload logic, it will completely explode ++ // etc later ++ this.unloadCallback(); ++ net.minecraft.server.ChunkSystem.onChunkNotBorder(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ this.postProcessGeneration(); ++ this.level.startTickingChunk(this); ++ net.minecraft.server.ChunkSystem.onChunkTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkNotTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ net.minecraft.server.ChunkSystem.onChunkNotTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ net.minecraft.server.ChunkSystem.onChunkEntityTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkNotEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ net.minecraft.server.ChunkSystem.onChunkNotEntityTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ // Paper end - new load callbacks ++ + // CraftBukkit start + public void loadCallback() { ++ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper + // Paper start - neighbour cache + int chunkX = this.chunkPos.x; + int chunkZ = this.chunkPos.z; +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + // Paper end - neighbour cache + org.bukkit.Server server = this.level.getCraftServer(); + this.level.getChunkSource().addLoadedChunk(this); // Paper ++ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + * no way of creating a CraftWorld/CraftServer at that point. + */ + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(this.bukkitChunk, this.needsDecoration)); ++ this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { + try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + } + + public void unloadCallback() { ++ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper + org.bukkit.Server server = this.level.getCraftServer(); ++ this.chunkHolder.getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system + org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved()); + server.getPluginManager().callEvent(unloadEvent); + // note: saving can be prevented, but not forced if no saving is actually required +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + }); + } + ++ public boolean isPostProcessingDone; // Paper - replace chunk loader system ++ + public void postProcessGeneration() { ++ try { // Paper - replace chunk loader system + ChunkPos chunkcoordintpair = this.getPos(); + + for (int i = 0; i < this.postProcessing.length; ++i) { +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + + this.pendingBlockEntities.clear(); + this.upgradeData.upgrade(this); ++ } finally { // Paper start - replace chunk loader system ++ this.isPostProcessingDone = true; ++ this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z); ++ } ++ // Paper end - replace chunk loader system + } + + @Nullable +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + } + + public ChunkHolder.FullChunkStatus getFullStatus() { +- return this.fullStatus == null ? ChunkHolder.FullChunkStatus.BORDER : (ChunkHolder.FullChunkStatus) this.fullStatus.get(); ++ return this.chunkHolder == null ? ChunkHolder.FullChunkStatus.INACCESSIBLE : this.chunkHolder.getChunkStatus(); // Paper - rewrite chunk system + } + + public void setFullStatus(Supplier levelTypeProvider) { +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 (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( ++ 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"); ++ ++ 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( ++ 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 + // Paper start - rewrite light impl + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); +@@ -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 { + nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) + } + +- 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) { + ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter + 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(); ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.flush(); // Paper - rewrite chunk system + } + + 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/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- 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 { + private static final String ENTITIES_TAG = "Entities"; + private static final String POSITION_TAG = "Position"; + public final ServerLevel level; +- private final IOWorker worker; ++ // Paper - rewrite chunk system + private final LongSet emptyChunks = new LongOpenHashSet(); +- public final ProcessorMailbox entityDeserializerQueue; ++ // Paper - rewrite chunk system + protected final DataFixer fixerUpper; + + public EntityStorage(ServerLevel world, Path path, DataFixer dataFixer, boolean dsync, Executor executor) { + this.level = world; + this.fixerUpper = dataFixer; +- this.entityDeserializerQueue = ProcessorMailbox.create(executor, "entity-deserializer"); +- this.worker = new IOWorker(path, dsync, "entities"); ++ // Paper - rewrite chunk system + } + + @Override + public CompletableFuture> loadEntities(ChunkPos pos) { +- return this.emptyChunks.contains(pos.toLong()) ? CompletableFuture.completedFuture(emptyChunk(pos)) : this.worker.loadAsync(pos).thenApplyAsync((nbt) -> { +- if (nbt.isEmpty()) { +- this.emptyChunks.add(pos.toLong()); +- return emptyChunk(pos); +- } else { +- try { +- ChunkPos chunkPos2 = readChunkPos(nbt.get()); +- if (!Objects.equals(pos, chunkPos2)) { +- LOGGER.error("Chunk file at {} is in the wrong location. (Expected {}, got {})", pos, pos, chunkPos2); +- } +- } catch (Exception var6) { +- LOGGER.warn("Failed to parse chunk {} position info", pos, var6); +- } +- +- CompoundTag compoundTag = this.upgradeChunkTag(nbt.get()); +- ListTag listTag = compoundTag.getList("Entities", 10); +- List list = EntityType.loadEntitiesRecursive(listTag, this.level).collect(ImmutableList.toImmutableList()); +- return new ChunkEntities<>(pos, list); +- } +- }, this.entityDeserializerQueue::tell); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - copy out read logic into readEntities ++ } ++ ++ // Paper start - rewrite chunk system ++ public static List readEntities(ServerLevel level, CompoundTag compoundTag) { ++ ListTag listTag = compoundTag.getList("Entities", 10); ++ List list = EntityType.loadEntitiesRecursive(listTag, level).collect(ImmutableList.toImmutableList()); ++ return list; + } ++ // Paper end - rewrite chunk system + +- private static ChunkPos readChunkPos(CompoundTag chunkNbt) { ++ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public + int[] is = chunkNbt.getIntArray("Position"); + return new ChunkPos(is[0], is[1]); + } +@@ -0,0 +0,0 @@ public class EntityStorage implements EntityPersistentStorage { + + @Override + public void storeEntities(ChunkEntities dataList) { ++ // Paper start - rewrite chunk system ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ // Paper end - rewrite chunk system + ChunkPos chunkPos = dataList.getPos(); + if (dataList.isEmpty()) { + if (this.emptyChunks.add(chunkPos.toLong())) { +- this.worker.store(chunkPos, (CompoundTag)null); ++ // Paper - rewrite chunk system + } + + } else { +- ListTag listTag = new ListTag(); +- dataList.getEntities().forEach((entity) -> { +- CompoundTag compoundTag = new CompoundTag(); +- if (entity.save(compoundTag)) { +- listTag.add(compoundTag); +- } +- +- }); +- CompoundTag compoundTag = new CompoundTag(); +- compoundTag.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); +- compoundTag.put("Entities", listTag); +- writeChunkPos(compoundTag, chunkPos); +- this.worker.store(chunkPos, compoundTag).exceptionally((ex) -> { +- LOGGER.error("Failed to store chunk {}", chunkPos, ex); +- return null; +- }); ++ // Paper - move into saveEntityChunk0 + this.emptyChunks.remove(chunkPos.toLong()); + } + } + ++ // Paper start - rewrite chunk system ++ public static void copyEntities(final CompoundTag from, final CompoundTag into) { ++ if (from == null) { ++ return; ++ } ++ final ListTag entitiesFrom = from.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); ++ if (entitiesFrom == null || entitiesFrom.isEmpty()) { ++ return; ++ } ++ ++ final ListTag entitiesInto = into.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); ++ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities ++ entitiesInto.addAll(0, entitiesFrom.copy()); // need to copy, this is coming from the save thread ++ } ++ ++ public static CompoundTag saveEntityChunk(List entities, ChunkPos chunkPos, ServerLevel level) { ++ return saveEntityChunk0(entities, chunkPos, level, false); ++ } ++ private static CompoundTag saveEntityChunk0(List entities, ChunkPos chunkPos, ServerLevel level, boolean force) { ++ if (!force && entities.isEmpty()) { ++ return null; ++ } ++ ListTag listTag = new ListTag(); ++ entities.forEach((entity) -> { // diff here: use entities parameter ++ CompoundTag compoundTag = new CompoundTag(); ++ if (entity.save(compoundTag)) { ++ listTag.add(compoundTag); ++ } ++ ++ }); ++ CompoundTag compoundTag = new CompoundTag(); ++ compoundTag.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); ++ compoundTag.put("Entities", listTag); ++ writeChunkPos(compoundTag, chunkPos); ++ ++ return !force && listTag.isEmpty() ? null : compoundTag; ++ } ++ // Paper end - rewrite chunk system ++ + @Override + public void flush(boolean sync) { +- this.worker.synchronize(sync).join(); +- this.entityDeserializerQueue.runAll(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + +- private CompoundTag upgradeChunkTag(CompoundTag chunkNbt) { ++ public static CompoundTag upgradeChunkTag(CompoundTag chunkNbt) { // Paper - public and static + int i = getVersion(chunkNbt); + return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, chunkNbt, i, SharedConstants.getCurrentVersion().getWorldVersion()); // Paper - route to new converter system + } +@@ -0,0 +0,0 @@ public class EntityStorage implements EntityPersistentStorage { + + @Override + public void close() throws IOException { +- this.worker.close(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + } +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 { + private final Path folder; + private final boolean sync; + +- RegionFileStorage(Path directory, boolean dsync) { ++ protected RegionFileStorage(Path directory, boolean dsync) { // Paper - protected constructor + this.folder = directory; + 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, nbt == null, true); // CraftBukkit // Paper // Paper start - rewrite chunk system ++ if (nbt == null && regionfile == null) { ++ return; ++ } ++ // Paper end - rewrite chunk system ++ 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; + private final Function factory; + private final DataFixer fixerUpper; + private final DataFixTypes type; +- private final RegistryAccess registryAccess; ++ public final RegistryAccess registryAccess; // Paper - rewrite chunk system + 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 void readColumn(ChunkPos pos) { +- Optional optional = this.tryRead(pos).join(); +- RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, this.registryAccess); +- this.readColumn(pos, registryOps, optional.orElse((CompoundTag)null)); ++ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + } + + 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 - rewrite chunk system ++ try { ++ return CompletableFuture.completedFuture(Optional.ofNullable(this.read(pos))); ++ } catch (Throwable thr) { ++ return CompletableFuture.failedFuture(thr); ++ } ++ // Paper end - rewrite chunk system + } + + private void readColumn(ChunkPos pos, DynamicOps ops, @Nullable T data) { ++ if (true) throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + if (data == null) { + for(int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); ++i) { + this.storage.put(getKey(pos, i), Optional.empty()); +@@ -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 { + } + + private static int getVersion(Dynamic dynamic) { +- return dynamic.get("DataVersion").asInt(1945); ++ return dynamic.get("DataVersion").asInt(1945); // Paper - diff on change, constant used in ChunkLoadTask + } + + public void flush(ChunkPos pos) { +@@ -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 - rewrite chunk system + } +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) { +- 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; ++ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper ++ // 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/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { + this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk); + return chunk; +- }), Util.backgroundExecutor()); ++ }), executor); // Paper - run with supplied executor + } + + private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) { +@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("wgen_fill_noise", () -> { + return this.doFill(blender, structureAccessor, noiseConfig, chunk, j, k); +- }), Util.backgroundExecutor()).whenCompleteAsync((ichunkaccess1, throwable) -> { ++ }), executor).whenCompleteAsync((ichunkaccess1, throwable) -> { // Paper - run with supplied executor + Iterator iterator = set.iterator(); + + while (iterator.hasNext()) { +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 { + + @Override + public boolean isEntitiesLoaded() { +- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(x, z)); ++ return this.getCraftWorld().getHandle().areEntitiesLoaded(io.papermc.paper.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system + } + + @Override +@@ -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 entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); +- } +- +- 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 entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); ++ return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -0,0 +0,0 @@ public final class CraftServer implements Server { + this.console.addLevel(internal); + + this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); +- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API ++ //internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system + + this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld())); + return internal.getWorld(); +@@ -0,0 +0,0 @@ public final class CraftServer implements Server { + } + + handle.getChunkSource().close(save); +- handle.entityManager.close(save); // SPIGOT-6722: close entityManager ++ // handle.entityManager.close(save); // SPIGOT-6722: close entityManager // Paper - rewrite chunk system + handle.convertable.close(); + } catch (Exception ex) { + this.getLogger().log(Level.SEVERE, null, ex); +@@ -0,0 +0,0 @@ public final class CraftServer implements Server { + + @Override + public boolean isPrimaryThread() { +- return Thread.currentThread().equals(console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog) ++ return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + } + + // Paper start +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +- playerChunk.getTickingChunkFuture().thenAccept(either -> { +- either.left().ifPresent(chunk -> { ++ // Paper start - rewrite player chunk loader ++ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getSendingChunk(); ++ if (chunk == null) { ++ return false; ++ } ++ // Paper end - rewrite player chunk loader + 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); + for (ServerPlayer player : playersInRange) { +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + player.connection.send(refreshPacket); + } +- }); +- }); ++ // Paper - rewrite player chunk loader + + return true; + } +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public Collection getPluginChunkTickets(int x, int z) { + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; +- SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); +- +- if (tickets == null) { +- return Collections.emptyList(); +- } +- +- ImmutableList.Builder ret = ImmutableList.builder(); +- for (Ticket ticket : tickets) { +- if (ticket.getType() == TicketType.PLUGIN_TICKET) { +- ret.add((Plugin) ticket.key); +- } +- } +- +- return ret.build(); ++ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system + } + + @Override +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + Map> ret = new HashMap<>(); + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; + +- for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { ++ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system + long chunkKey = chunkTickets.getLongKey(); + SortedArraySet> tickets = chunkTickets.getValue(); + +@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World { + // Spigot start + @Override + public int getViewDistance() { +- return world.spigotConfig.viewDistance; ++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management + } + + @Override + public int getSimulationDistance() { +- return world.spigotConfig.simulationDistance; ++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - replace old player chunk management + } + // Spigot end ++ // Paper start - view distance api ++ @Override ++ public void setViewDistance(int viewDistance) { ++ if (viewDistance < 2 || viewDistance > 32) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; ++ chunkMap.setViewDistance(viewDistance); ++ } ++ ++ @Override ++ public void setSimulationDistance(int simulationDistance) { ++ if (simulationDistance < 2 || simulationDistance > 32) { ++ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap; ++ chunkMap.setTickViewDistance(simulationDistance); ++ } ++ ++ @Override ++ public int getNoTickViewDistance() { ++ return this.getViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ this.setViewDistance(viewDistance); ++ } ++ ++ @Override ++ public int getSendViewDistance() { ++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); ++ } ++ ++ @Override ++ public void setSendViewDistance(int viewDistance) { ++ getHandle().getChunkSource().chunkMap.playerChunkManager.setSendDistance(viewDistance); ++ } ++ // Paper end - view distance api + + // Spigot start + private final org.bukkit.World.Spigot spigot = new org.bukkit.World.Spigot() +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + this.firstPlayed = System.currentTimeMillis(); + } + ++ // Paper start - implement view distances ++ @Override ++ public int getViewDistance() { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetNoTickViewDistance(); ++ } ++ return data.getTargetNoTickViewDistance(); ++ } ++ ++ @Override ++ public void setViewDistance(int viewDistance) { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetNoTickViewDistance(viewDistance); ++ } ++ ++ @Override ++ public int getSimulationDistance() { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetTickViewDistance(); ++ } ++ return data.getTargetTickViewDistance(); ++ } ++ ++ @Override ++ public void setSimulationDistance(int simulationDistance) { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetTickViewDistance(simulationDistance); ++ } ++ ++ @Override ++ public int getNoTickViewDistance() { ++ return this.getViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ this.setViewDistance(viewDistance); ++ } ++ ++ @Override ++ public int getSendViewDistance() { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ return chunkMap.playerChunkManager.getTargetSendDistance(); ++ } ++ return data.getTargetSendViewDistance(); ++ } ++ ++ @Override ++ public void setSendViewDistance(int viewDistance) { ++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap; ++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle()); ++ if (data == null) { ++ throw new IllegalStateException("Player is not attached to world"); ++ } ++ ++ data.setTargetSendViewDistance(viewDistance); ++ } ++ // Paper end - implement view distances ++ + public GameProfile getProfile() { + return this.getHandle().getGameProfile(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -0,0 +0,0 @@ public class CustomChunkGenerator extends InternalChunkGenerator { + return ichunkaccess1; + }; + +- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function); ++ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), executor) : future.thenApply(function); // Paper - run with supplied executor + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +@@ -0,0 +0,0 @@ public class DummyGeneratorAccess implements WorldGenLevel { + public boolean destroyBlock(BlockPos pos, boolean drop, Entity breakingEntity, int maxUpdateDepth) { + return false; // SPIGOT-6515 + } ++ ++ // Paper start ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return java.util.Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Paper end + } +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 ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -0,0 +0,0 @@ public class ActivationRange + ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange ); + ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange ); + +- world.getEntities().get(maxBB, ActivationRange::activateEntity); ++ // Paper start ++ java.util.List entities = world.getEntities((Entity)null, maxBB, null); ++ for (int i = 0; i < entities.size(); i++) { ++ Entity entity = entities.get(i); ++ ActivationRange.activateEntity(entity); ++ } ++ // Paper end + } + MinecraftTimings.entityActivationCheckTimer.stopTiming(); + } +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -0,0 +0,0 @@ public class AsyncCatcher + + public static void catchOp(String reason) + { +- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper ++ if ( !io.papermc.paper.util.TickThread.isTickThread() ) // Paper // Paper - rewrite chunk system + { + throw new IllegalStateException( "Asynchronous " + reason + "!" ); + } +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 @@ import java.util.logging.Logger; + import net.minecraft.server.MinecraftServer; + import org.bukkit.Bukkit; + +-public class WatchdogThread extends Thread ++public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system + { + + private static WatchdogThread instance; +@@ -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!):" ); ++ 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/Rewrite-dataconverter-system.patch b/patches/server/Rewrite-dataconverter-system.patch index d0957b44fe..11e02f9037 100644 --- a/patches/server/Rewrite-dataconverter-system.patch +++ b/patches/server/Rewrite-dataconverter-system.patch @@ -22675,8 +22675,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 - nbttagcompound = NbtUtils.update(this.fixerUpper, DataFixTypes.CHUNK, nbttagcompound, i, 1493); + ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { - synchronized (this.persistentDataLock) { // Paper - Async chunk loading LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier); + @@ -0,0 +0,0 @@ public class ChunkStorage implements AutoCloseable { // Spigot end @@ -22703,7 +22703,7 @@ diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorag 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 +@@ -0,0 +0,0 @@ public class SectionStorage implements AutoCloseable { int j = getVersion(dynamic); int k = SharedConstants.getCurrentVersion().getWorldVersion(); boolean bl = j != k; diff --git a/patches/server/Rewrite-entity-bounding-box-lookup-calls.patch b/patches/server/Rewrite-entity-bounding-box-lookup-calls.patch deleted file mode 100644 index adf00cd03c..0000000000 --- a/patches/server/Rewrite-entity-bounding-box-lookup-calls.patch +++ /dev/null @@ -1,1300 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 17 Jun 2021 19:55:02 -0700 -Subject: [PATCH] Rewrite entity bounding box lookup calls - -For whatever reason, Mojang thought it was OK to make this system -scale logn relative to the number of entity sections loaded. -On top of that, they do a hashtable lookup per section - before -this was just a basic array access. - -This patch brings back entity slices for lookup only. - -diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.world; -+ -+import com.destroystokyo.paper.util.maplist.EntityList; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.entity.boss.EnderDragonPart; -+import net.minecraft.world.entity.boss.enderdragon.EnderDragon; -+import net.minecraft.world.phys.AABB; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.function.Predicate; -+ -+public final class ChunkEntitySlices { -+ -+ protected final int minSection; -+ protected final int maxSection; -+ protected final int chunkX; -+ protected final int chunkZ; -+ protected final ServerLevel world; -+ -+ protected final EntityCollectionBySection allEntities; -+ protected final EntityCollectionBySection hardCollidingEntities; -+ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; -+ protected final EntityList entities = new EntityList(); -+ -+ public ChunkHolder.FullChunkStatus status; -+ -+ // TODO implement container search optimisations -+ -+ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus status, -+ final int minSection, final int maxSection) { // inclusive, inclusive -+ this.minSection = minSection; -+ this.maxSection = maxSection; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.world = world; -+ -+ this.allEntities = new EntityCollectionBySection(this); -+ this.hardCollidingEntities = new EntityCollectionBySection(this); -+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); -+ -+ this.status = status; -+ } -+ -+ // Paper start - optimise CraftChunk#getEntities -+ public org.bukkit.entity.Entity[] getChunkEntities() { -+ List ret = new java.util.ArrayList<>(); -+ final Entity[] entities = this.entities.getRawData(); -+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { -+ final Entity entity = entities[i]; -+ if (entity == null) { -+ continue; -+ } -+ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); -+ if (bukkit != null && bukkit.isValid()) { -+ ret.add(bukkit); -+ } -+ } -+ -+ return ret.toArray(new org.bukkit.entity.Entity[0]); -+ } -+ // Paper end - optimise CraftChunk#getEntities -+ -+ public boolean isEmpty() { -+ return this.entities.size() == 0; -+ } -+ -+ private void updateTicketLevels() { -+ final Entity[] entities = this.entities.getRawData(); -+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { -+ final Entity entity = entities[i]; -+ entity.chunkStatus = this.status; -+ } -+ } -+ -+ public synchronized void updateStatus(final ChunkHolder.FullChunkStatus status) { -+ this.status = status; -+ this.updateTicketLevels(); -+ } -+ -+ public synchronized void addEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.add(entity)) { -+ return; -+ } -+ entity.chunkStatus = this.status; -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.addEntity(entity, sectionIndex); -+ -+ if (entity.hardCollides()) { -+ this.hardCollidingEntities.addEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().addEntity(entity, sectionIndex); -+ } -+ } -+ } -+ -+ public synchronized void removeEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.remove(entity)) { -+ return; -+ } -+ entity.chunkStatus = ChunkHolder.FullChunkStatus.INACCESSIBLE; -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.removeEntity(entity, sectionIndex); -+ -+ if (entity.hardCollides()) { -+ this.hardCollidingEntities.removeEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().removeEntity(entity, sectionIndex); -+ } -+ } -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.hardCollidingEntities.getEntities(except, box, into, predicate); -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); -+ } -+ -+ protected EntityCollectionBySection initClass(final Class clazz) { -+ final EntityCollectionBySection ret = new EntityCollectionBySection(this); -+ -+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { -+ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; -+ if (sectionEntities == null) { -+ continue; -+ } -+ -+ final Entity[] storage = sectionEntities.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (clazz.isInstance(entity)) { -+ ret.addEntity(entity, sectionIndex); -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); -+ } else { -+ synchronized (this) { -+ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); -+ } -+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); -+ } -+ } -+ -+ public synchronized void updateEntity(final Entity entity) { -+ /*// TODO -+ if (prev aabb != entity.getBoundingBox()) { -+ this.entityMap.delete(entity, prev aabb); -+ this.entityMap.insert(entity, prev aabb = entity.getBoundingBox()); -+ }*/ -+ } -+ -+ protected static final class BasicEntityList { -+ -+ protected static final Entity[] EMPTY = new Entity[0]; -+ protected static final int DEFAULT_CAPACITY = 4; -+ -+ protected E[] storage; -+ protected int size; -+ -+ public BasicEntityList() { -+ this(0); -+ } -+ -+ public BasicEntityList(final int cap) { -+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); -+ } -+ -+ public boolean isEmpty() { -+ return this.size == 0; -+ } -+ -+ public int size() { -+ return this.size; -+ } -+ -+ private void resize() { -+ if (this.storage == EMPTY) { -+ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; -+ } else { -+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); -+ } -+ } -+ -+ public void add(final E entity) { -+ final int idx = this.size++; -+ if (idx >= this.storage.length) { -+ this.resize(); -+ this.storage[idx] = entity; -+ } else { -+ this.storage[idx] = entity; -+ } -+ } -+ -+ public int indexOf(final E entity) { -+ final E[] storage = this.storage; -+ -+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { -+ if (storage[i] == entity) { -+ return i; -+ } -+ } -+ -+ return -1; -+ } -+ -+ public boolean remove(final E entity) { -+ final int idx = this.indexOf(entity); -+ if (idx == -1) { -+ return false; -+ } -+ -+ final int size = --this.size; -+ final E[] storage = this.storage; -+ if (idx != size) { -+ System.arraycopy(storage, idx + 1, storage, idx, size - idx); -+ } -+ -+ storage[size] = null; -+ -+ return true; -+ } -+ -+ public boolean has(final E entity) { -+ return this.indexOf(entity) != -1; -+ } -+ } -+ -+ protected static final class EntityCollectionBySection { -+ -+ protected final ChunkEntitySlices manager; -+ protected final long[] nonEmptyBitset; -+ protected final BasicEntityList[] entitiesBySection; -+ protected int count; -+ -+ public EntityCollectionBySection(final ChunkEntitySlices manager) { -+ this.manager = manager; -+ -+ final int sectionCount = manager.maxSection - manager.minSection + 1; -+ -+ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE -+ this.entitiesBySection = new BasicEntityList[sectionCount]; -+ } -+ -+ public void addEntity(final Entity entity, final int sectionIndex) { -+ BasicEntityList list = this.entitiesBySection[sectionIndex]; -+ -+ if (list != null && list.has(entity)) { -+ return; -+ } -+ -+ if (list == null) { -+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); -+ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); -+ } -+ -+ list.add(entity); -+ ++this.count; -+ } -+ -+ public void removeEntity(final Entity entity, final int sectionIndex) { -+ final BasicEntityList list = this.entitiesBySection[sectionIndex]; -+ -+ if (list == null || !list.remove(entity)) { -+ return; -+ } -+ -+ --this.count; -+ -+ if (list.isEmpty()) { -+ this.entitiesBySection[sectionIndex] = null; -+ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); -+ } -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ // TODO use the bitset -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(entity)) { -+ continue; -+ } -+ -+ into.add(entity); -+ } -+ } -+ } -+ -+ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ // TODO use the bitset -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate == null || predicate.test(entity)) { -+ into.add(entity); -+ } // else: continue to test the ender dragon parts -+ -+ if (entity instanceof EnderDragon) { -+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { -+ if (part == except || !part.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(part)) { -+ continue; -+ } -+ -+ into.add(part); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ // TODO use the bitset -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate == null || predicate.test(entity)) { -+ into.add(entity); -+ } // else: continue to test the ender dragon parts -+ -+ if (entity instanceof EnderDragon) { -+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { -+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(part)) { -+ continue; -+ } -+ -+ into.add(part); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ // TODO use the bitset -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test((T)entity)) { -+ continue; -+ } -+ -+ into.add((T)entity); -+ } -+ } -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/world/EntitySliceManager.java b/src/main/java/io/papermc/paper/world/EntitySliceManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/world/EntitySliceManager.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.world; -+ -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.WorldUtil; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.phys.AABB; -+import java.util.List; -+import java.util.concurrent.locks.StampedLock; -+import java.util.function.Predicate; -+ -+public final class EntitySliceManager { -+ -+ protected static final int REGION_SHIFT = 5; -+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; -+ protected static final int REGION_SIZE = 1 << REGION_SHIFT; -+ -+ public final ServerLevel world; -+ -+ private final StampedLock stateLock = new StampedLock(); -+ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(64, 0.7f); -+ -+ private final int minSection; // inclusive -+ private final int maxSection; // inclusive -+ -+ protected final Long2ObjectOpenHashMap statusMap = new Long2ObjectOpenHashMap<>(); -+ { -+ this.statusMap.defaultReturnValue(ChunkHolder.FullChunkStatus.INACCESSIBLE); -+ } -+ -+ public EntitySliceManager(final ServerLevel world) { -+ this.world = world; -+ this.minSection = WorldUtil.getMinSection(world); -+ this.maxSection = WorldUtil.getMaxSection(world); -+ } -+ -+ public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) { -+ if (newStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { -+ this.statusMap.remove(CoordinateUtils.getChunkKey(x, z)); -+ } else { -+ this.statusMap.put(CoordinateUtils.getChunkKey(x, z), newStatus); -+ final ChunkEntitySlices slices = this.getChunk(x, z); -+ if (slices != null) { -+ slices.updateStatus(newStatus); -+ } -+ } -+ } -+ -+ public synchronized void addEntity(final Entity entity) { -+ final BlockPos pos = entity.blockPosition(); -+ final int sectionX = pos.getX() >> 4; -+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); -+ final int sectionZ = pos.getZ() >> 4; -+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); -+ slices.addEntity(entity, sectionY); -+ -+ entity.sectionX = sectionX; -+ entity.sectionY = sectionY; -+ entity.sectionZ = sectionZ; -+ } -+ -+ public synchronized void removeEntity(final Entity entity) { -+ final ChunkEntitySlices slices = this.getChunk(entity.sectionX, entity.sectionZ); -+ slices.removeEntity(entity, entity.sectionY); -+ if (slices.isEmpty()) { -+ this.removeChunk(entity.sectionX, entity.sectionZ); -+ } -+ } -+ -+ public void moveEntity(final Entity entity) { -+ final BlockPos newPos = entity.blockPosition(); -+ final int newSectionX = newPos.getX() >> 4; -+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); -+ final int newSectionZ = newPos.getZ() >> 4; -+ -+ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { -+ return; -+ } -+ -+ synchronized (this) { -+ // are we changing chunks? -+ if (newSectionX != entity.sectionX || newSectionZ != entity.sectionZ) { -+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); -+ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); -+ synchronized (old) { -+ old.removeEntity(entity, entity.sectionY); -+ if (old.isEmpty()) { -+ this.removeChunk(entity.sectionX, entity.sectionZ); -+ } -+ } -+ -+ synchronized (slices) { -+ slices.addEntity(entity, newSectionY); -+ -+ entity.sectionX = newSectionX; -+ entity.sectionY = newSectionY; -+ entity.sectionZ = newSectionZ; -+ } -+ } else { -+ final ChunkEntitySlices slices = this.getChunk(newSectionX, newSectionZ); -+ // same chunk -+ synchronized (slices) { -+ slices.removeEntity(entity, entity.sectionY); -+ slices.addEntity(entity, newSectionY); -+ } -+ entity.sectionY = newSectionY; -+ } -+ } -+ -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { -+ continue; -+ } -+ -+ chunk.getEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { -+ continue; -+ } -+ -+ chunk.getHardCollidingEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { -+ continue; -+ } -+ -+ chunk.getEntities(type, box, (List)into, (Predicate)predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { -+ continue; -+ } -+ -+ chunk.getEntities(clazz, except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ if (region == null) { -+ return null; -+ } -+ -+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); -+ } -+ -+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ ChunkEntitySlices ret; -+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { -+ ret = new ChunkEntitySlices(this.world, chunkX, chunkZ, this.statusMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)), -+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); -+ -+ this.addChunk(chunkX, chunkZ, ret); -+ -+ return ret; -+ } -+ -+ return ret; -+ } -+ -+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { -+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ); -+ final long attempt = this.stateLock.tryOptimisticRead(); -+ if (attempt != 0L) { -+ try { -+ final ChunkSlicesRegion ret = this.regions.get(key); -+ -+ if (this.stateLock.validate(attempt)) { -+ return ret; -+ } -+ } catch (final Error error) { -+ throw error; -+ } catch (final Throwable thr) { -+ // ignore -+ } -+ } -+ -+ this.stateLock.readLock(); -+ try { -+ return this.regions.get(key); -+ } finally { -+ this.stateLock.tryUnlockRead(); -+ } -+ } -+ -+ public synchronized void removeChunk(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ final ChunkSlicesRegion region = this.regions.get(key); -+ final int remaining = region.remove(relIndex); -+ -+ if (remaining == 0) { -+ this.stateLock.writeLock(); -+ try { -+ this.regions.remove(key); -+ } finally { -+ this.stateLock.tryUnlockWrite(); -+ } -+ } -+ } -+ -+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ ChunkSlicesRegion region = this.regions.get(key); -+ if (region != null) { -+ region.add(relIndex, slices); -+ } else { -+ region = new ChunkSlicesRegion(); -+ region.add(relIndex, slices); -+ this.stateLock.writeLock(); -+ try { -+ this.regions.put(key, region); -+ } finally { -+ this.stateLock.tryUnlockWrite(); -+ } -+ } -+ } -+ -+ public static final class ChunkSlicesRegion { -+ -+ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; -+ protected int sliceCount; -+ -+ public ChunkEntitySlices get(final int index) { -+ return this.slices[index]; -+ } -+ -+ public int remove(final int index) { -+ final ChunkEntitySlices slices = this.slices[index]; -+ if (slices == null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = null; -+ -+ return --this.sliceCount; -+ } -+ -+ public void add(final int index, final ChunkEntitySlices slices) { -+ final ChunkEntitySlices curr = this.slices[index]; -+ if (curr != null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = slices; -+ -+ ++this.sliceCount; -+ } -+ } -+} -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 { - DataFixer datafixer = minecraftserver.getFixerUpper(); - EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); - -- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); -+ this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper - StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); - int j = this.spigotConfig.viewDistance; // Spigot - int k = this.spigotConfig.simulationDistance; // Spigot -diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java -+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -@@ -0,0 +0,0 @@ public class WorldGenRegion implements WorldGenLevel { - public long nextSubTickCount() { - return this.subTickCount.getAndIncrement(); - } -+ -+ // Paper start -+ // No-op, this class doesn't provide entity access -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ return Collections.emptyList(); -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - } - // Paper end - make end portalling safe - -+ // Paper start -+ /** -+ * Overriding this field will cause memory leaks. -+ */ -+ private final boolean hardCollides; -+ -+ private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); -+ { -+ /* // Goodbye, broken on reobf... -+ Boolean hardCollides = cachedOverrides.get(this.getClass()); -+ if (hardCollides == null) { -+ try { -+ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); -+ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); -+ if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) -+ || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { -+ hardCollides = Boolean.TRUE; -+ } else { -+ hardCollides = Boolean.FALSE; -+ } -+ cachedOverrides.put(this.getClass(), hardCollides); -+ } -+ catch (ThreadDeath thr) { throw thr; } -+ catch (Throwable thr) { -+ // shouldn't happen, just explode -+ throw new RuntimeException(thr); -+ } -+ } */ -+ this.hardCollides = this instanceof Boat -+ || this instanceof net.minecraft.world.entity.monster.Shulker -+ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart -+ || this.shouldHardCollide(); -+ } -+ -+ // plugins can override -+ protected boolean shouldHardCollide() { -+ return false; -+ } -+ -+ public final boolean hardCollides() { -+ return this.hardCollides; -+ } -+ -+ public net.minecraft.server.level.ChunkHolder.FullChunkStatus chunkStatus; -+ -+ public int sectionX = Integer.MIN_VALUE; -+ public int sectionY = Integer.MIN_VALUE; -+ public int sectionZ = Integer.MIN_VALUE; -+ // Paper end -+ - public Entity(EntityType type, Level world) { - this.id = Entity.ENTITY_COUNTER.incrementAndGet(); - this.passengers = ImmutableList.of(); -@@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - return InteractionResult.PASS; - } - -- public boolean canCollideWith(Entity other) { -+ public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override - return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); - } - -- public boolean canBeCollidedWith() { -+ public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override - return false; - } - -diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/EntityGetter.java -+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -0,0 +0,0 @@ import net.minecraft.world.phys.shapes.Shapes; - import net.minecraft.world.phys.shapes.VoxelShape; - - public interface EntityGetter { -+ -+ // Paper start -+ List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); -+ -+ void getEntities(Entity except, AABB box, Predicate predicate, List into); -+ -+ void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); -+ -+ void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, -+ Predicate predicate); -+ // Paper end -+ - List getEntities(@Nullable Entity except, AABB box, Predicate predicate); - - List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); -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 { - this.entityLimiter = new org.spigotmc.TickLimiter(spigotConfig.entityMaxTickTime); - this.tileLimiter = new org.spigotmc.TickLimiter(spigotConfig.tileMaxTickTime); - this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray -+ this.entitySliceManager = new io.papermc.paper.world.EntitySliceManager((ServerLevel)this); // Paper - } - - // Paper start -@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { - this.getProfiler().incrementCounter("getEntities"); - List list = Lists.newArrayList(); -- -- this.getEntities().get(box, (entity1) -> { -- if (entity1 != except && predicate.test(entity1)) { -- list.add(entity1); -- } -- -- if (entity1 instanceof EnderDragon) { -- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); -- int i = aentitycomplexpart.length; -- -- for (int j = 0; j < i; ++j) { -- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; -- -- if (entity1 != except && predicate.test(entitycomplexpart)) { -- list.add(entitycomplexpart); -- } -- } -- } -- -- }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper -+ this.entitySliceManager.getEntities(except, box, list, predicate); // Paper - optimise this call - return list; - } - -@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.getProfiler().incrementCounter("getEntities"); - List list = Lists.newArrayList(); - -- this.getEntities().get(filter, box, (entity) -> { -- if (predicate.test(entity)) { -- list.add(entity); -- } -- -- if (entity instanceof EnderDragon) { -- EnderDragon entityenderdragon = (EnderDragon) entity; -- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities(); -- int i = aentitycomplexpart.length; -- -- for (int j = 0; j < i; ++j) { -- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; -- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error -- -- if (t0 != null && predicate.test(t0)) { -- list.add(t0); -- } -- } -+ // Paper start - optimise this call -+ if (filter instanceof net.minecraft.world.entity.EntityType) { -+ this.entitySliceManager.getEntities((net.minecraft.world.entity.EntityType)filter, box, list, predicate); -+ } else { -+ Predicate test = (obj) -> { -+ return filter.tryCast(obj) != null; -+ }; -+ predicate = predicate == null ? test : test.and((Predicate)predicate); -+ Class base; -+ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { -+ this.entitySliceManager.getEntities((Entity) null, box, (List)list, (Predicate)predicate); -+ } else { -+ this.entitySliceManager.getEntities(base, null, box, (List)list, (Predicate)predicate); // Paper - optimise this call - } -- -- }); -+ } -+ // Paper end - optimise this call - return list; - } - -@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public long nextSubTickCount() { - return (long) (this.subTickCount++); - } -+ -+ // Paper start -+ protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; -+ -+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { -+ io.papermc.paper.world.ChunkEntitySlices slices = this.entitySliceManager.getChunk(chunkX, chunkZ); -+ if (slices == null) { -+ return new org.bukkit.entity.Entity[0]; -+ } -+ return slices.getChunkEntities(); -+ } -+ -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ List ret = new java.util.ArrayList<>(); -+ this.entitySliceManager.getEntities(except, box, ret, predicate); -+ return ret; -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) { -+ this.entitySliceManager.getEntities(except, box, into, predicate); -+ } -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { -+ this.entitySliceManager.getHardCollidingEntities(except, box, into, predicate); -+ } -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, -+ Predicate predicate) { -+ this.entitySliceManager.getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); -+ } -+ -+ @Override -+ public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { -+ List ret = new java.util.ArrayList<>(); -+ this.entitySliceManager.getEntities(entityClass, null, box, ret, predicate); -+ return ret; -+ } -+ // Paper end - } -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 final Long2ObjectMap chunkLoadStatuses = new Long2ObjectOpenHashMap(); - private final LongSet chunksToUnload = new LongOpenHashSet(); - private final Queue> loadingInbox = Queues.newConcurrentLinkedQueue(); -+ public final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - -- public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess) { -+ public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess, io.papermc.paper.world.EntitySliceManager entitySliceManager) { // Paper -+ this.entitySliceManager = entitySliceManager; // Paper - this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility); - this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN); - this.chunkLoadStatuses.defaultReturnValue(PersistentEntitySectionManager.ChunkLoadStatus.FRESH); -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - EntitySection entitysection = this.sectionStorage.getOrCreateSection(i); - - entitysection.add(entity); -+ this.entitySliceManager.addEntity((Entity)entity); // Paper - entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, i, entitysection)); - if (!existing) { - this.callbacks.onCreated(entity); -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous chunk ticking status update"); // Paper - Visibility visibility = Visibility.fromFullChunkStatus(levelType); - -+ this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Paper - this.updateChunkStatus(chunkPos, visibility); - } - -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - long i = SectionPos.asLong(blockposition); - - if (i != this.currentSectionKey) { -+ PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Paper - Visibility visibility = this.currentSection.getStatus(); - - if (!this.currentSection.remove(this.entity)) { -@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager implements A - if (!this.currentSection.remove(this.entity)) { - PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (destroying due to {})", new Object[]{this.entity, SectionPos.of(this.currentSectionKey), reason}); - } -+ PersistentEntitySectionManager.this.entitySliceManager.removeEntity((Entity)this.entity); // Paper - - Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, this.currentSection.getStatus()); - -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 { - long pair = ChunkPos.asLong(x, z); - - if (entityManager.areEntitiesLoaded(pair)) { -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -+ return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this - } - - entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading -@@ -0,0 +0,0 @@ public class CraftChunk implements Chunk { - } - } - -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -+ return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -@@ -0,0 +0,0 @@ public class DummyGeneratorAccess implements WorldGenLevel { - public boolean destroyBlock(BlockPos pos, boolean drop, Entity breakingEntity, int maxUpdateDepth) { - return false; // SPIGOT-6515 - } -+ -+ // Paper start -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ return java.util.Collections.emptyList(); -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} -+ // Paper end - } -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 -+++ b/src/main/java/org/spigotmc/ActivationRange.java -@@ -0,0 +0,0 @@ public class ActivationRange - 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); -+ for (int i = 0; i < entities.size(); i++) { -+ Entity entity = entities.get(i); -+ ActivationRange.activateEntity(entity); -+ } -+ // Paper end - } - MinecraftTimings.entityActivationCheckTimer.stopTiming(); - } diff --git a/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch b/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch index 67cb9a57ce..559e975483 100644 --- a/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch +++ b/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch @@ -66,7 +66,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 WatchdogThread(long timeoutTime, boolean restart) { @@ -75,7 +75,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 this.timeoutTime = timeoutTime; this.restart = restart; } -@@ -0,0 +0,0 @@ public class WatchdogThread extends Thread +@@ -0,0 +0,0 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa { Logger log = Bukkit.getServer().getLogger(); log.log( Level.SEVERE, "------------------------------" ); @@ -93,12 +93,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 // if ( net.minecraft.world.level.Level.lastPhysicsProblem != null ) { -@@ -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 Spigot!):" ); + 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/Rewrite-the-light-engine.patch b/patches/server/Starlight.patch similarity index 95% rename from patches/server/Rewrite-the-light-engine.patch rename to patches/server/Starlight.patch index f222b39710..b12090bf56 100644 --- a/patches/server/Rewrite-the-light-engine.patch +++ b/patches/server/Starlight.patch @@ -1,40 +1,9 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Wed, 28 Oct 2020 16:51:55 -0700 -Subject: [PATCH] Rewrite the light engine +Subject: [PATCH] Starlight -The standard vanilla light engine is plagued by -awful performance. Paper's changes to the light engine -help a bit, however they appear to cause some lighting -errors - most easily noticed in coral generation. - -The vanilla light engine's is too abstract to be modified - -so an entirely new implementation is required to fix the -performance and lighting errors. - -The new implementation is designed primarily to optimise -light level propagations (increase and decrease). Unlike -the vanilla light engine, this implementation tracks more -information per queued value when performing a -breadth first search. Vanilla just tracks coordinate, which -means every time they handle a queued value, they must -also determine the coordinate's target light level -from its neighbours - very wasteful, especially considering -these checks read neighbour block data. -The new light engine tracks both position and target level, -as well as whether the target block needs to be read at all -(for checking sided propagation). So, the work done per coordinate -is significantly reduced because no work is done for calculating -the target level. -In my testing, the block get calls were reduced by approximately -an order of magnitude. However, the light read checks were only -reduced by approximately 2x - but this is fine, light read checks -are extremely cheap compared to block gets. - -Generation testing showed that the new light engine improved -total generation (not lighting itself, but the whole generation process) -by 2x. According to cpu time, the light engine itself spent 10x less time -lighting chunks for generation. +See https://github.com/PaperMC/Starlight diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java new file mode 100644 @@ -4357,53 +4326,99 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + +} -diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +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/subcommands/FixLightCommand.java -+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java -@@ -0,0 +0,0 @@ import net.minecraft.server.level.ServerLevel; - import net.minecraft.server.level.ServerPlayer; - import net.minecraft.server.level.ThreadedLevelLightEngine; - import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; - import net.minecraft.world.level.chunk.LevelChunk; - import org.bukkit.command.CommandSender; - import org.bukkit.craftbukkit.entity.CraftPlayer; -@@ -0,0 +0,0 @@ import org.checkerframework.checker.nullness.qual.Nullable; - import org.checkerframework.framework.qual.DefaultQualifier; +--- 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 static net.kyori.adventure.text.Component.text; + import io.papermc.paper.command.subcommands.EntityCommand; ++import io.papermc.paper.command.subcommands.FixLightCommand; + import io.papermc.paper.command.subcommands.HeapDumpCommand; + import io.papermc.paper.command.subcommands.ReloadCommand; + import io.papermc.paper.command.subcommands.VersionCommand; +@@ -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("fixlight"), new FixLightCommand()); + + 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/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.PaperSubcommand; ++import java.util.ArrayDeque; ++import java.util.Deque; ++import net.minecraft.server.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.ThreadedLevelLightEngine; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++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; - -@@ -0,0 +0,0 @@ public final class FixLightCommand implements PaperSubcommand { - sender.sendMessage(text("Radius cannot be negative!", RED)); - return; - } -- final int maxRadius = 5; -+ final int maxRadius = 32; // Paper - MOOOOOORE - radius = Math.min(maxRadius, parsed); - if (radius != parsed) { - post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); -@@ -0,0 +0,0 @@ public final class FixLightCommand implements PaperSubcommand { - ServerPlayer handle = player.getHandle(); - ServerLevel world = (ServerLevel) handle.level; - ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); -+ // Paper start - rewrite light engine -+ if (true) { -+ this.starlightFixLight(handle, world, lightengine, radius, post); ++import static net.kyori.adventure.text.format.NamedTextColor.GREEN; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class FixLightCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.doFixLight(sender, args); ++ return true; ++ } ++ ++ private void doFixLight(final CommandSender sender, final String[] args) { ++ if (!(sender instanceof Player)) { ++ sender.sendMessage(text("Only players can use this command", RED)); + return; + } -+ // Paper end - rewrite light engine - - net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation()); - Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); - updateLight(sender, world, lightengine, queue, post); - } - -+ // Paper start - rewrite light engine ++ @Nullable Runnable post = null; ++ int radius = 2; ++ if (args.length > 0) { ++ try { ++ final int parsed = Integer.parseInt(args[0]); ++ if (parsed < 0) { ++ sender.sendMessage(text("Radius cannot be negative!", RED)); ++ return; ++ } ++ final int maxRadius = 32; ++ radius = Math.min(maxRadius, parsed); ++ if (radius != parsed) { ++ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); ++ } ++ } catch (final Exception e) { ++ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); ++ return; ++ } ++ } ++ ++ CraftPlayer player = (CraftPlayer) sender; ++ ServerPlayer handle = player.getHandle(); ++ ServerLevel world = (ServerLevel) handle.level; ++ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); ++ this.starlightFixLight(handle, world, lightengine, radius, post); ++ } ++ + private void starlightFixLight( + final ServerPlayer sender, + final ServerLevel world, @@ -4411,10 +4426,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + final int radius, + final @Nullable Runnable done + ) { -+ long start = System.nanoTime(); -+ java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos ++ final long start = System.nanoTime(); ++ final java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + -+ int[] pending = new int[1]; ++ final int[] pending = new int[1]; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { + final ChunkPos chunkPos = iterator.next(); + @@ -4428,16 +4443,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + ++pending[0]; + } + -+ int[] relitChunks = new int[1]; ++ final int[] relitChunks = new int[1]; + lightengine.relight(chunks, -+ (ChunkPos chunkPos) -> { ++ (final ChunkPos chunkPos) -> { + ++relitChunks[0]; + sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( + text("Relit chunk ", BLUE), text(chunkPos.toString()), + text(", progress: ", BLUE), text((int) (Math.round(100.0 * (double) (relitChunks[0]) / (double) pending[0])) + "%") + )); + }, -+ (int totalRelit) -> { ++ (final int totalRelit) -> { + final long end = System.nanoTime(); + final long diff = Math.round(1.0e-6 * (end - start)); + sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( @@ -4447,14 +4462,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + if (done != null) { + done.run(); + } -+ }); ++ } ++ ); + sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); + } -+ // Paper end - rewrite light engine -+ - private void updateLight( - final CommandSender sender, - final ServerLevel world, ++} 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 @@ -4478,9 +4490,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop mainThreadExecutor; + public final BlockableEventLoop mainThreadExecutor; // Paper - public - final java.util.concurrent.Executor mainInvokingExecutor; // Paper public ChunkGenerator generator; private RandomState randomState; + public final Supplier overworldDataStorage; 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 @@ -4489,8 +4501,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } public void removeTicketsOnClosing() { -- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.ASYNC_LOAD, TicketType.REQUIRED_LOAD); // Paper - add additional tickets to preserve -+ ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.ASYNC_LOAD, TicketType.REQUIRED_LOAD, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve +- 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.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); while (objectiterator.hasNext()) { @@ -4529,7 +4541,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { - super(chunkProvider, true, hasBlockLight); + super(chunkProvider, false, false); // Paper - destroy vanilla light engine state - this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper + this.chunkMap = chunkStorage; this.sorterMailbox = executor; this.taskMailbox = processor; + // Paper start - replace light engine impl @@ -4539,7 +4551,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end - replace light engine impl + } + -+// Paper start - replace light engine impl ++ // Paper start - replace light engine impl + protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { + return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); + } @@ -4664,7 +4676,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + @Override + public boolean hasLightWork() { + // route to new light engine -+ return this.theLightEngine.hasUpdates() || !this.queue.isEmpty(); ++ return this.theLightEngine.hasUpdates(); } + @Override @@ -4785,40 +4797,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + // Paper end - replace light engine impl ChunkPos chunkPos = chunk.getPos(); - // Paper start - //ichunkaccess.b(false); // Don't need to disable this + chunk.setLightCorrect(false); + this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public void tryScheduleUpdate() { -- if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper +- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { + if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine this.taskMailbox.tell(() -> { this.runUpdate(); this.scheduled.set(false); @@ -0,0 +0,0 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - if (queue.poll(pre, post)) { - pre.forEach(Runnable::run); - pre.clear(); -- super.runUpdates(Integer.MAX_VALUE, true, true); -+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine - post.forEach(Runnable::run); - post.clear(); - } else { - // might have level updates to go still -- super.runUpdates(Integer.MAX_VALUE, true, true); -+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine } - // Paper end - } + + objectListIterator.back(j); +- super.runUpdates(Integer.MAX_VALUE, true, true); ++ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine + + for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) { + Pair pair2 = objectListIterator.next(); 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 final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail + public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting public static TicketType create(String name, Comparator argumentComparator) { @@ -4833,13 +4839,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 this.offsetType = (BlockBehaviour.OffsetType) blockbase_info.offsetType.apply(this.asState()); + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Paper } - // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time - private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; + + // Paper start @@ -0,0 +0,0 @@ public abstract class BlockBehaviour { - protected boolean isTicking; - protected FluidState fluid; + return this.shapeExceedsCube; + } // Paper end -+ // Paper start ++ // Paper start - starlight + protected int opacityIfCached = -1; + // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] + public final int getOpacityIfCached() { @@ -4850,15 +4856,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public final boolean isConditionallyFullOpaque() { + return this.conditionallyFullOpaque; + } -+ // Paper end ++ // Paper end - starlight public void initCache() { - this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() -@@ -0,0 +0,0 @@ public abstract class BlockBehaviour { + if (!this.getBlock().hasDynamicShape()) { this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); } this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here -+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light ++ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light } @@ -4914,6 +4919,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biome, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable BlendingData blendingData) { this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +@@ -0,0 +0,0 @@ public class ChunkStatus { + return this.chunkType; + } + ++ // Paper start ++ public static ChunkStatus getStatus(String name) { ++ try { ++ // We need this otherwise we return EMPTY for invalid names ++ ResourceLocation key = new ResourceLocation(name); ++ return Registry.CHUNK_STATUS.getOptional(key).orElse(null); ++ } catch (Exception ex) { ++ return null; // invalid name ++ } ++ } ++ // Paper end + public static ChunkStatus byName(String id) { + return (ChunkStatus) Registry.CHUNK_STATUS.get(ResourceLocation.tryParse(id)); + } diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java @@ -4922,6 +4949,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 this.biome = holder; } ++ // Paper start - starlight + @Override + public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { + return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); @@ -4953,10 +4981,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + @Override + public void setBlockEmptinessMap(final boolean[] emptinessMap) {} ++ // Paper end - starlight + - // Paper start @Override - public BlockState getBlockState(int x, int y, int z) { + public BlockState getBlockState(BlockPos pos) { + return Blocks.VOID_AIR.defaultBlockState(); diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java @@ -5017,7 +5046,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { 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, 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 + super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), inhabitedTime, sectionArrayInitializer, blendingData); + // 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)); @@ -5086,13 +5115,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public ChunkSerializer() {} - // Paper start - guard against serializing mismatching coordinates + public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { @@ -0,0 +0,0 @@ public class ChunkSerializer { } UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY; - boolean flag = nbt.getBoolean("isLightOn"); -+ boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper ++ boolean flag = getStatus(nbt) != null && getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper ListTag nbttaglist = nbt.getList("sections", 10); int i = world.getSectionsCount(); LevelChunkSection[] achunksection = new LevelChunkSection[i]; @@ -5100,8 +5129,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 ServerChunkCache chunkproviderserver = world.getChunkSource(); LevelLightEngine lightengine = chunkproviderserver.getLightEngine(); + // Paper start -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); + boolean canReadSky = world.dimensionType().hasSkyLight(); @@ -5124,40 +5153,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 - if (flag3 || flag4) { - if (!flag2) { +- lightengine.retainData(chunkPos, true); +- flag2 = true; +- } +- + // Paper start - rewrite the light engine + if (flag) { + try { -+ if ((flag3 || flag4) && !flag2) { -+ // Paper end - rewrite the light engine - 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; - } - + int y = sectionData.getByte("Y"); ++ // Paper end - rewrite the light engine if (flag3) { -- // 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 +- lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight")), true); + // Paper start - rewrite the light engine + // this is where our diff is + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety + } else { + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); ++ // Paper end - rewrite the light engine } -+ // Paper end - rewrite the light engine if (flag4) { -- // 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 +- lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight")), true); + // Paper start - rewrite the light engine + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. @@ -5167,13 +5183,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety + } else if (flag1) { + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); - } -+ // Paper end - rewrite the light engine ++ // Paper end - rewrite the light engine ++ } ++ + // Paper start - rewrite the light engine + } catch (Exception ex) { + LOGGER.warn("Failed to load light data for chunk " + chunkPos + " in world '" + world.getWorld().getName() + "', light will be regenerated", ex); + flag = false; -+ } + } + // Paper end - rewrite light engine } } @@ -5197,18 +5214,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 object1 = protochunk; protochunk.setInhabitedTime(l); @@ -0,0 +0,0 @@ public class ChunkSerializer { - DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; - DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; + // CraftBukkit end -- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { -+ for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Paper - don't run loop, we don't need to - light data is per chunk now - DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i)); - DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i)); - -@@ -0,0 +0,0 @@ public class ChunkSerializer { - } - public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, @org.checkerframework.checker.nullness.qual.Nullable AsyncSaveData asyncsavedata) { - // Paper end + public static CompoundTag write(ServerLevel world, ChunkAccess chunk) { + // Paper start - rewrite light impl + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); @@ -5222,17 +5230,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { int j = chunk.getSectionIndexFromSectionY(i); boolean flag1 = j >= 0 && j < achunksection.length; -- // 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 +- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); +- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); + // Paper - replace light engine - if (flag1 || nibblearray != null || nibblearray1 != null) { @@ -5287,4 +5286,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) } - // Paper start + ListTag nbttaglist1 = new ListTag(); +@@ -0,0 +0,0 @@ public class ChunkSerializer { + })); + } + ++ // Paper start ++ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) { ++ if (compound == null) { ++ return null; ++ } ++ ++ // Note: Copied from below ++ return ChunkStatus.getStatus(compound.getString("Status")); ++ } ++ // Paper end ++ + public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) { + return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkStatus.ChunkType.PROTOCHUNK; + } diff --git a/patches/server/Stop-copy-on-write-operations-for-updating-light-dat.patch b/patches/server/Stop-copy-on-write-operations-for-updating-light-dat.patch index be3a3be127..7302583027 100644 --- a/patches/server/Stop-copy-on-write-operations-for-updating-light-dat.patch +++ b/patches/server/Stop-copy-on-write-operations-for-updating-light-dat.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Mon, 27 Apr 2020 04:05:38 -0700 Subject: [PATCH] Stop copy-on-write operations for updating light data diff --git a/patches/server/System-property-for-disabling-watchdoge.patch b/patches/server/System-property-for-disabling-watchdoge.patch index c5213c8c5a..8b145e16e3 100644 --- a/patches/server/System-property-for-disabling-watchdoge.patch +++ b/patches/server/System-property-for-disabling-watchdoge.patch @@ -8,7 +8,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 while ( !this.stopping ) { // diff --git a/patches/server/Time-scoreboard-search.patch b/patches/server/Time-scoreboard-search.patch index e89edc891a..de75179ecd 100644 --- a/patches/server/Time-scoreboard-search.patch +++ b/patches/server/Time-scoreboard-search.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf +From: Spottedleaf Date: Tue, 21 Apr 2020 01:53:22 -0700 Subject: [PATCH] Time scoreboard search diff --git a/patches/server/Use-a-Queue-for-Queueing-Commands.patch b/patches/server/Use-a-Queue-for-Queueing-Commands.patch index d01068c14f..dfe1e3de50 100644 --- a/patches/server/Use-a-Queue-for-Queueing-Commands.patch +++ b/patches/server/Use-a-Queue-for-Queueing-Commands.patch @@ -19,11 +19,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 private QueryThreadGs4 queryThreadGs4; public final RconConsoleSource rconConsoleSource; @@ -0,0 +0,0 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - } - - public void handleConsoleInput(String command, CommandSourceStack commandSource) { + return; + } + // Paper end - rewrite chunk system - this.consoleInput.add(new ConsoleInput(command, commandSource)); -+ this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); ++ this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - use proper queue } public void handleConsoleInputs() { diff --git a/patches/server/Use-distance-map-to-optimise-entity-tracker.patch b/patches/server/Use-distance-map-to-optimise-entity-tracker.patch index 8ac3f47623..6b85b5fb6c 100644 --- a/patches/server/Use-distance-map-to-optimise-entity-tracker.patch +++ b/patches/server/Use-distance-map-to-optimise-entity-tracker.patch @@ -40,46 +40,49 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end - use distance map to optimise tracker void addPlayerToDistanceMaps(ServerPlayer player) { - int chunkX = MCUtil.getChunkCoordinate(player.getX()); - int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.playerMobDistanceMap.add(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); + } + // Paper end - per player mob spawning + // Paper start - use distance map to optimise entity tracker + for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { + com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; + int trackRange = this.entityTrackerTrackRanges[i]; + -+ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); ++ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, net.minecraft.server.ChunkSystem.getSendViewDistance(player))); + } + // Paper end - use distance map to optimise entity tracker - // Note: players need to be explicitly added to distance maps before they can be updated - this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - // Paper start - per player mob spawning -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } void removePlayerFromDistanceMaps(ServerPlayer player) { - +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.playerMobDistanceMap.remove(player); + } + // Paper end - per player mob spawning + // Paper start - use distance map to optimise tracker + for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { + this.playerEntityTrackerTrackMaps[i].remove(player); + } + // Paper end - use distance map to optimise tracker - // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - this.playerMobSpawnMap.remove(player); - this.playerChunkTickRangeMap.remove(player); + } + + void updateMaps(ServerPlayer player) { @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int chunkX = MCUtil.getChunkCoordinate(player.getX()); - int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); - // Note: players need to be explicitly added to distance maps before they can be updated + this.playerMobDistanceMap.update(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); + } + // Paper end - per player mob spawning + // Paper start - use distance map to optimise entity tracker + for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { + com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; + int trackRange = this.entityTrackerTrackRanges[i]; + -+ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance())); ++ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, net.minecraft.server.ChunkSystem.getSendViewDistance(player))); + } + // Paper end - use distance map to optimise entity tracker - this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { + } + // Paper end + // Paper start @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.regionManagers.add(this.dataRegionManager); // Paper end @@ -193,49 +196,53 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 List list1 = this.level.players(); ObjectIterator objectiterator = this.entityMap.values().iterator(); @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + })); + // Paper end DebugPackets.sendPoiPacketsForChunk(this.level, chunk.getPos()); - List list = Lists.newArrayList(); - List list1 = Lists.newArrayList(); +- List list = Lists.newArrayList(); +- List list1 = Lists.newArrayList(); - ObjectIterator objectiterator = this.entityMap.values().iterator(); -+ // Paper start - optimise entity tracker -+ // use the chunk entity list, not the whole trackedEntities map... -+ Entity[] entities = chunk.entities.getRawData(); -+ for (int i = 0, size = chunk.entities.size(); i < size; ++i) { -+ Entity entity = entities[i]; -+ if (entity == player) { -+ continue; -+ } -+ ChunkMap.TrackedEntity tracker = this.entityMap.get(entity.getId()); -+ if (tracker != null) { // dumb plugins... move on... -+ tracker.updatePlayer(player); -+ } - +- - while (objectiterator.hasNext()) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - Entity entity = playerchunkmap_entitytracker.entity; -+ // keep the vanilla logic here - this is REQUIRED or else passengers and their vehicles disappear! -+ // (and god knows what the leash thing is) - +- - if (entity != player && entity.chunkPosition().equals(chunk.getPos())) { - playerchunkmap_entitytracker.updatePlayer(player); - if (entity instanceof Mob && ((Mob) entity).getLeashHolder() != null) { - list.add(entity); - } -+ if (entity instanceof Mob && ((Mob)entity).getLeashHolder() != null) { -+ list.add(entity); -+ } - +- - if (!entity.getPassengers().isEmpty()) { - list1.add(entity); - } -+ if (!entity.getPassengers().isEmpty()) { -+ list1.add(entity); - } - } -+ // Paper end - optimise entity tracker +- } +- } +- +- Iterator iterator; +- Entity entity1; +- +- if (!list.isEmpty()) { +- iterator = list.iterator(); +- +- while (iterator.hasNext()) { +- entity1 = (Entity) iterator.next(); +- player.connection.send(new ClientboundSetEntityLinkPacket(entity1, ((Mob) entity1).getLeashHolder())); +- } +- } +- +- if (!list1.isEmpty()) { +- iterator = list1.iterator(); +- +- while (iterator.hasNext()) { +- entity1 = (Entity) iterator.next(); +- player.connection.send(new ClientboundSetPassengersPacket(entity1)); +- } +- } ++ // Paper - no longer needed - this was used to account for clients bugging out since they needed a chunk to store entities, but they no longer need a chunk + + } - Iterator iterator; - Entity entity1; @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lastSectionPos = SectionPos.of((EntityAccess) entity); } @@ -292,9 +299,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -0,0 +0,0 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { - } - // Paper end + public boolean updatingSectionStatus = false; + // Paper end + // Paper start - optimise entity tracking + final org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = org.spigotmc.TrackingRange.getTrackingRangeType(this); + @@ -327,10 +334,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); + } + // Paper end - optimise entity tracking -+ + public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); - this.passengers = ImmutableList.of(); diff --git a/src/main/java/org/spigotmc/TrackingRange.java b/src/main/java/org/spigotmc/TrackingRange.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/org/spigotmc/TrackingRange.java diff --git a/patches/server/WorldCreator-keepSpawnLoaded.patch b/patches/server/WorldCreator-keepSpawnLoaded.patch index 779f2b3d4d..506033c048 100644 --- a/patches/server/WorldCreator-keepSpawnLoaded.patch +++ b/patches/server/WorldCreator-keepSpawnLoaded.patch @@ -14,5 +14,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + internal.keepSpawnInMemory = creator.keepSpawnLoaded().toBooleanOrElse(internal.getWorld().getKeepSpawnInMemory()); // Paper this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); - internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API + //internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system diff --git a/patches/server/fix-converting-txt-to-json-file.patch b/patches/server/fix-converting-txt-to-json-file.patch index 5497932c76..122d140896 100644 --- a/patches/server/fix-converting-txt-to-json-file.patch +++ b/patches/server/fix-converting-txt-to-json-file.patch @@ -58,4 +58,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor public void placeNewPlayer(Connection connection, ServerPlayer player) { - player.isRealPlayer = true; // Paper - Chunk priority + player.isRealPlayer = true; // Paper diff --git a/patches/server/implement-optional-per-player-mob-spawns.patch b/patches/server/implement-optional-per-player-mob-spawns.patch index 8f726f4f33..e6eac3d384 100644 --- a/patches/server/implement-optional-per-player-mob-spawns.patch +++ b/patches/server/implement-optional-per-player-mob-spawns.patch @@ -261,20 +261,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 int viewDistance; + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper - // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() - public final CallbackExecutor callbackExecutor = new CallbackExecutor(); + // Paper - rewrite chunk system + @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider int chunkX = MCUtil.getChunkCoordinate(player.getX()); int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); // Note: players need to be explicitly added to distance maps before they can be updated + // Paper start - per player mob spawning + if (this.playerMobDistanceMap != null) { -+ this.playerMobDistanceMap.add(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); ++ this.playerMobDistanceMap.add(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); + } + // Paper end - per player mob spawning } void removePlayerFromDistanceMaps(ServerPlayer player) { + this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader + // Paper start - per player mob spawning + if (this.playerMobDistanceMap != null) { @@ -284,12 +285,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } void updateMaps(ServerPlayer player) { - int chunkX = MCUtil.getChunkCoordinate(player.getX()); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); // Note: players need to be explicitly added to distance maps before they can be updated + this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader + // Paper start - per player mob spawning + if (this.playerMobDistanceMap != null) { -+ this.playerMobDistanceMap.update(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance()); ++ this.playerMobDistanceMap.update(player, chunkX, chunkZ, net.minecraft.server.ChunkSystem.getTickViewDistance(player)); + } + // Paper end - per player mob spawning } @@ -304,9 +306,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 protected ChunkGenerator generator() { @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } + }); } - // Paper end + + // Paper start + public void updatePlayerMobTypeMap(Entity entity) { + if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { @@ -331,26 +333,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return entityPlayer.mobCounts[mobCategory.ordinal()]; + } + // Paper end - ++ private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); -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 { - - } - -+ // Paper start -+ public int getSimulationDistance() { -+ return this.simulationDistance; -+ } -+ // Paper end -+ - public int getNaturalSpawnChunkCount() { - this.naturalSpawnChunkCounter.runAllUpdates(); - return this.naturalSpawnChunkCounter.chunks.size(); + double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8); 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 @@ -397,8 +383,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 this.maxHealthCache = this.getMaxHealth(); + this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper } - // Paper start - Chunk priority - public BlockPos getPointInFront(double inFront) { + + // Yes, this doesn't match Vanilla, but it's the best we can do for now. diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java diff --git a/patches/server/incremental-chunk-and-player-saving.patch b/patches/server/incremental-chunk-and-player-saving.patch index 06c9d6f97a..80afeea9f5 100644 --- a/patches/server/incremental-chunk-and-player-saving.patch +++ b/patches/server/incremental-chunk-and-player-saving.patch @@ -52,188 +52,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 io.papermc.paper.util.CachedLists.reset(); // Paper // Paper start - move executeAll() into full server tick timing try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { -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 { - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInMobSpawnRange; - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInChunkTickRange; - // Paper end - optimise anyPlayerCloseEnoughForSpawning -+ long lastAutoSaveTime; // Paper - incremental autosave -+ long inactiveTimeStart; // Paper - incremental autosave - - public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { - this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); -@@ -0,0 +0,0 @@ public class ChunkHolder { - boolean flag2 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); - boolean flag3 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); - -+ boolean prevHasBeenLoaded = this.wasAccessibleSinceLastSave; // Paper - this.wasAccessibleSinceLastSave |= flag3; -+ // Paper start - incremental autosave -+ if (this.wasAccessibleSinceLastSave & !prevHasBeenLoaded) { -+ long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime; -+ if (timeSinceAutoSave < 0) { -+ // safest bet is to assume autosave is needed here -+ timeSinceAutoSave = this.chunkMap.level.paperConfig().chunks.autoSaveInterval.value(); -+ } -+ this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave; -+ this.chunkMap.autoSaveQueue.add(this); -+ } -+ // Paper end - if (!flag2 && flag3) { - int expectCreateCount = ++this.fullChunkCreateCount; // Paper - this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); -@@ -0,0 +0,0 @@ public class ChunkHolder { - } - - public void refreshAccessibility() { -+ boolean prev = this.wasAccessibleSinceLastSave; // Paper - this.wasAccessibleSinceLastSave = ChunkHolder.getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER); -+ // Paper start - incremental autosave -+ if (prev != this.wasAccessibleSinceLastSave) { -+ if (this.wasAccessibleSinceLastSave) { -+ long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime; -+ if (timeSinceAutoSave < 0) { -+ // safest bet is to assume autosave is needed here -+ timeSinceAutoSave = this.chunkMap.level.paperConfig().chunks.autoSaveInterval.value(); -+ } -+ this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave; -+ this.chunkMap.autoSaveQueue.add(this); -+ } else { -+ this.inactiveTimeStart = this.chunkMap.level.getGameTime(); -+ this.chunkMap.autoSaveQueue.remove(this); -+ } -+ } -+ // Paper end -+ } -+ -+ // Paper start - incremental autosave -+ public boolean setHasBeenLoaded() { -+ this.wasAccessibleSinceLastSave = getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER); -+ return this.wasAccessibleSinceLastSave; - } -+ // Paper end - - public void replaceProtoChunk(ImposterProtoChunk chunk) { - for (int i = 0; i < this.futures.length(); ++i) { -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 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp - import net.minecraft.world.level.storage.DimensionDataStorage; - import net.minecraft.world.level.storage.LevelStorageSource; - import net.minecraft.world.phys.Vec3; -+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; // Paper - import org.apache.commons.lang3.mutable.MutableBoolean; - import org.apache.commons.lang3.mutable.MutableObject; - import org.slf4j.Logger; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - } - -+ // Paper start - incremental autosave -+ final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((playerchunk1, playerchunk2) -> { -+ int timeCompare = Long.compare(playerchunk1.lastAutoSaveTime, playerchunk2.lastAutoSaveTime); -+ if (timeCompare != 0) { -+ return timeCompare; -+ } -+ -+ return Long.compare(MCUtil.getCoordinateKey(playerchunk1.pos), MCUtil.getCoordinateKey(playerchunk2.pos)); -+ }); -+ -+ protected void saveIncrementally() { -+ int savedThisTick = 0; -+ // optimized since we search far less chunks to hit ones that need to be saved -+ List reschedule = new java.util.ArrayList<>(this.level.paperConfig().chunks.maxAutoSaveChunksPerTick); -+ long currentTick = this.level.getGameTime(); -+ long maxSaveTime = currentTick - this.level.paperConfig().chunks.autoSaveInterval.value(); -+ -+ for (Iterator iterator = this.autoSaveQueue.iterator(); iterator.hasNext();) { -+ ChunkHolder playerchunk = iterator.next(); -+ if (playerchunk.lastAutoSaveTime > maxSaveTime) { -+ break; -+ } -+ -+ iterator.remove(); -+ -+ ChunkAccess ichunkaccess = playerchunk.getChunkToSave().getNow(null); -+ if (ichunkaccess instanceof LevelChunk) { -+ boolean shouldSave = ((LevelChunk)ichunkaccess).lastSaveTime <= maxSaveTime; -+ -+ if (shouldSave && this.save(ichunkaccess) && this.level.entityManager.storeChunkSections(playerchunk.pos.toLong(), entity -> {})) { -+ ++savedThisTick; -+ -+ if (!playerchunk.setHasBeenLoaded()) { -+ // do not fall through to reschedule logic -+ playerchunk.inactiveTimeStart = currentTick; -+ if (savedThisTick >= this.level.paperConfig().chunks.maxAutoSaveChunksPerTick) { -+ break; -+ } -+ continue; -+ } -+ } -+ } -+ -+ reschedule.add(playerchunk); -+ -+ if (savedThisTick >= this.level.paperConfig().chunks.maxAutoSaveChunksPerTick) { -+ break; -+ } -+ } -+ -+ for (int i = 0, len = reschedule.size(); i < len; ++i) { -+ ChunkHolder playerchunk = reschedule.get(i); -+ playerchunk.lastAutoSaveTime = this.level.getGameTime(); -+ this.autoSaveQueue.add(playerchunk); -+ } -+ } -+ // Paper end -+ - protected void saveAllChunks(boolean flush) { - // Paper start - do not overload I/O threads with too much work when saving - int[] saved = new int[1]; -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - int l = 0; -- Iterator objectiterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper -- -- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { -- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { -- ++l; -- } -- } -+ // Paper - incremental chunk and player saving - - } - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - this.level.unload(chunk); - } -+ this.autoSaveQueue.remove(holder); // Paper - - this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); - this.lightEngine.tryScheduleUpdate(); -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - asyncSaveData, chunk); - - chunk.setUnsaved(false); -+ chunk.setLastSaved(this.level.getGameTime()); // Paper - track last saved time - } - // Paper end - -@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - if (!chunk.isUnsaved()) { - return false; - } else { -+ chunk.setLastSaved(this.level.getGameTime()); // Paper - track save time - chunk.setUnsaved(false); - ChunkPos chunkcoordintpair = chunk.getPos(); - 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 @@ -294,8 +112,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { - ServerChunkCache chunkproviderserver = this.getChunkSource(); - + // Paper start - rewrite chunk system - add close param + this.save(progressListener, flush, savingDisabled, false); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -344,32 +162,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } MinecraftTimings.savePlayers.stopTiming(); // Paper return null; }); // Paper - ensure main -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - public LevelHeightAccessor getHeightAccessorForGeneration() { - return this; - } -+ public void setLastSaved(long ticks) {} // Paper - - public static record TicksToSave(SerializableTickContainer blocks, SerializableTickContainer fluids) { - -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { - private final Int2ObjectMap gameEventDispatcherSections; - private final LevelChunkTicks blockTicks; - private final LevelChunkTicks fluidTicks; -+ // Paper start - track last save time -+ public long lastSaveTime; -+ public void setLastSaved(long ticks) { -+ this.lastSaveTime = ticks; -+ } -+ // Paper end - - public LevelChunk(Level world, ChunkPos pos) { - this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null);