diff --git a/Spigot-Server-Patches/0003-MC-Dev-fixes.patch b/Spigot-Server-Patches/0003-MC-Dev-fixes.patch index 7efbfc9e94..3d6faa5239 100644 --- a/Spigot-Server-Patches/0003-MC-Dev-fixes.patch +++ b/Spigot-Server-Patches/0003-MC-Dev-fixes.patch @@ -1,4 +1,4 @@ -From 7f5c894288d4476ea577befbbb98c3eca8b59530 Mon Sep 17 00:00:00 2001 +From 742264d273660d238ce3a7fa3bd46201db8a82fb Mon Sep 17 00:00:00 2001 From: Aikar Date: Wed, 30 Mar 2016 19:36:20 -0400 Subject: [PATCH] MC Dev fixes @@ -357,6 +357,31 @@ index 0dda7aaf69..4e20cfba41 100644 } } +diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java +index a343a7b31d..4b3e0c0f01 100644 +--- a/src/main/java/net/minecraft/server/RegionFileSection.java ++++ b/src/main/java/net/minecraft/server/RegionFileSection.java +@@ -82,9 +82,9 @@ public class RegionFileSection extends RegionFi + Optional optional = this.d(i); + + if (optional.isPresent()) { +- return (MinecraftSerializable) optional.get(); ++ return optional.get(); // Paper - decompile fix + } else { +- R r0 = (MinecraftSerializable) this.f.apply(() -> { ++ R r0 = this.f.apply(() -> { // Paper - decompile fix + this.a(i); + }); + +@@ -123,7 +123,7 @@ public class RegionFileSection extends RegionFi + for (int l = 0; l < 16; ++l) { + long i1 = SectionPosition.a(chunkcoordintpair, l).v(); + Optional optional = optionaldynamic.get(Integer.toString(l)).get().map((dynamic2) -> { +- return (MinecraftSerializable) this.e.apply(() -> { ++ return this.e.apply(() -> { // Paper - decompile fix + this.a(i1); + }, dynamic2); + }); diff --git a/src/main/java/net/minecraft/server/RegistryBlockID.java b/src/main/java/net/minecraft/server/RegistryBlockID.java index 7f89562e90..4efcb8b595 100644 --- a/src/main/java/net/minecraft/server/RegistryBlockID.java @@ -455,6 +480,28 @@ index c23a366b2c..0430ca5353 100644 } } +diff --git a/src/main/java/net/minecraft/server/VillagePlace.java b/src/main/java/net/minecraft/server/VillagePlace.java +index b0e6ad773e..3169590641 100644 +--- a/src/main/java/net/minecraft/server/VillagePlace.java ++++ b/src/main/java/net/minecraft/server/VillagePlace.java +@@ -157,7 +157,7 @@ public class VillagePlace extends RegionFileSection { + } + + private static boolean a(ChunkSection chunksection) { +- Stream stream = VillagePlaceType.f(); ++ Stream stream = VillagePlaceType.f(); // Paper - decompile fix + + chunksection.getClass(); + return stream.anyMatch(chunksection::a); +@@ -215,7 +215,7 @@ public class VillagePlace extends RegionFileSection { + + private final Predicate d; + +- private Occupancy(Predicate predicate) { ++ private Occupancy(Predicate predicate) { // Paper - decompile fix + this.d = predicate; + } + diff --git a/src/main/java/net/minecraft/server/VillagerTrades.java b/src/main/java/net/minecraft/server/VillagerTrades.java index 2fbb1f8df9..8cee460bd3 100644 --- a/src/main/java/net/minecraft/server/VillagerTrades.java @@ -507,5 +554,5 @@ index 0b950aae63..f5f540032f 100644 t0.a(nbttagcompound.getCompound("data")); -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0380-Async-Chunk-placeholder.patch b/Spigot-Server-Patches/0380-Async-Chunk-placeholder.patch deleted file mode 100644 index 7ef750cdb3..0000000000 --- a/Spigot-Server-Patches/0380-Async-Chunk-placeholder.patch +++ /dev/null @@ -1,63 +0,0 @@ -From 34e683e36fe14ee82ad3a444ffadd17470462f5f Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 6 May 2019 12:29:24 -0700 -Subject: [PATCH] Async Chunk placeholder - -Until we figure out Mojang's ticket system. - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 91b65fde0..afdb6956b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -18,6 +18,7 @@ import java.util.Objects; - import java.util.Random; - import java.util.Set; - import java.util.UUID; -+import java.util.concurrent.CompletableFuture; - import java.util.function.Predicate; - import it.unimi.dsi.fastutil.longs.Long2ObjectMap; - import it.unimi.dsi.fastutil.objects.ObjectSortedSet; -@@ -2257,6 +2258,40 @@ public class CraftWorld implements World { - return (nearest == null) ? null : new Location(this, nearest.getX(), nearest.getY(), nearest.getZ()); - } - -+ // Paper start -+ private Chunk getChunkAtGen(int x, int z, boolean gen) { -+ // copied from loadChunk() -+ // this function is identical except we do not add a plugin ticket -+ IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, gen || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); -+ -+ // If generate = false, but the chunk already exists, we will get this back. -+ if (chunk instanceof ProtoChunkExtension) { -+ // We then cycle through again to get the full chunk immediately, rather than after the ticket addition -+ chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.FULL, true); -+ } -+ -+ if (chunk instanceof net.minecraft.server.Chunk) { -+ return ((net.minecraft.server.Chunk)chunk).bukkitChunk; -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) { -+ // TODO placeholder -+ if (Bukkit.isPrimaryThread()) { -+ return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen)); -+ } else { -+ CompletableFuture ret = new CompletableFuture<>(); -+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { -+ ret.complete(getChunkAtGen(x, z, gen)); -+ }); -+ return ret; -+ } -+ } -+ // Paper end -+ - // Spigot start - @Override - public int getViewDistance() { --- -2.22.0 - diff --git a/Spigot-Server-Patches/0381-Fix-CB-call-to-changed-postToMainThread-method.patch b/Spigot-Server-Patches/0380-Fix-CB-call-to-changed-postToMainThread-method.patch similarity index 91% rename from Spigot-Server-Patches/0381-Fix-CB-call-to-changed-postToMainThread-method.patch rename to Spigot-Server-Patches/0380-Fix-CB-call-to-changed-postToMainThread-method.patch index bbf9e36582..276ea42449 100644 --- a/Spigot-Server-Patches/0381-Fix-CB-call-to-changed-postToMainThread-method.patch +++ b/Spigot-Server-Patches/0380-Fix-CB-call-to-changed-postToMainThread-method.patch @@ -1,4 +1,4 @@ -From ac0590e2d5af6b8fa96a15894b434ddc7a3458d4 Mon Sep 17 00:00:00 2001 +From 4248f97eb65fef4cc66def1ab94ce98b5c26d154 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Fri, 10 May 2019 18:38:19 +0100 Subject: [PATCH] Fix CB call to changed postToMainThread method @@ -18,5 +18,5 @@ index 7680b88024..4187ba05bd 100644 @Override -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0382-Fix-sounds-when-item-frames-are-modified-MC-123450.patch b/Spigot-Server-Patches/0381-Fix-sounds-when-item-frames-are-modified-MC-123450.patch similarity index 95% rename from Spigot-Server-Patches/0382-Fix-sounds-when-item-frames-are-modified-MC-123450.patch rename to Spigot-Server-Patches/0381-Fix-sounds-when-item-frames-are-modified-MC-123450.patch index f98732a1f1..fda4ce860a 100644 --- a/Spigot-Server-Patches/0382-Fix-sounds-when-item-frames-are-modified-MC-123450.patch +++ b/Spigot-Server-Patches/0381-Fix-sounds-when-item-frames-are-modified-MC-123450.patch @@ -1,4 +1,4 @@ -From 5c0d14c68d85c4dd031f920b56997708fff20905 Mon Sep 17 00:00:00 2001 +From c0175bc340b6ecab247b4972d98d21cc1a25e7ae Mon Sep 17 00:00:00 2001 From: Phoenix616 Date: Sat, 27 Apr 2019 20:00:43 +0100 Subject: [PATCH] Fix sounds when item frames are modified (MC-123450) @@ -32,5 +32,5 @@ index 799036f268..9ad180d946 100644 this.entity = frame; } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0383-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch b/Spigot-Server-Patches/0382-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch similarity index 96% rename from Spigot-Server-Patches/0383-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch rename to Spigot-Server-Patches/0382-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch index a1be9e7bb8..750b7a8141 100644 --- a/Spigot-Server-Patches/0383-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch +++ b/Spigot-Server-Patches/0382-Fix-CraftServer-isPrimaryThread-and-MinecraftServer-.patch @@ -1,4 +1,4 @@ -From aeef538a6f37143d4cacf2d4c66298232d6a19e4 Mon Sep 17 00:00:00 2001 +From 0e44f004120cb2ed3216a97092e6da88f4233340 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 @@ -42,5 +42,5 @@ index b89486beb1..7a8ab7d401 100644 @Override -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0384-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch b/Spigot-Server-Patches/0383-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch similarity index 96% rename from Spigot-Server-Patches/0384-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch rename to Spigot-Server-Patches/0383-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch index 3af35ba8bc..4be009cd56 100644 --- a/Spigot-Server-Patches/0384-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch +++ b/Spigot-Server-Patches/0383-Fix-issues-with-entity-loss-due-to-unloaded-chunks.patch @@ -1,4 +1,4 @@ -From 0e9dbe2c59db9edfc72829af874179d9852437d3 Mon Sep 17 00:00:00 2001 +From fe0d3a1329061e1007cac3067f00b4a3066cb1e2 Mon Sep 17 00:00:00 2001 From: Aikar Date: Fri, 28 Sep 2018 21:49:53 -0400 Subject: [PATCH] Fix issues with entity loss due to unloaded chunks @@ -41,5 +41,5 @@ index 192b3be1f0..82c3bc60d0 100644 if (!(ichunkaccess instanceof Chunk)) { return false; -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0385-Duplicate-UUID-Resolve-Option.patch b/Spigot-Server-Patches/0384-Duplicate-UUID-Resolve-Option.patch similarity index 99% rename from Spigot-Server-Patches/0385-Duplicate-UUID-Resolve-Option.patch rename to Spigot-Server-Patches/0384-Duplicate-UUID-Resolve-Option.patch index 9e21e1967c..7fb07c4ed2 100644 --- a/Spigot-Server-Patches/0385-Duplicate-UUID-Resolve-Option.patch +++ b/Spigot-Server-Patches/0384-Duplicate-UUID-Resolve-Option.patch @@ -1,4 +1,4 @@ -From 0330cf125621b330638b4ecb80560a26fc409200 Mon Sep 17 00:00:00 2001 +From a55ea9fd05f0520b014878afb447c0a092fc0a93 Mon Sep 17 00:00:00 2001 From: Aikar Date: Sat, 21 Jul 2018 14:27:34 -0400 Subject: [PATCH] Duplicate UUID Resolve Option @@ -244,5 +244,5 @@ index 82c3bc60d0..a6d0635ec1 100644 logger.error("Overwrote an existing entity " + old + " with " + entity); if (DEBUG_ENTITIES) { -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0386-improve-CraftWorld-isChunkLoaded.patch b/Spigot-Server-Patches/0385-improve-CraftWorld-isChunkLoaded.patch similarity index 89% rename from Spigot-Server-Patches/0386-improve-CraftWorld-isChunkLoaded.patch rename to Spigot-Server-Patches/0385-improve-CraftWorld-isChunkLoaded.patch index 851c39e02c..b314fa30c5 100644 --- a/Spigot-Server-Patches/0386-improve-CraftWorld-isChunkLoaded.patch +++ b/Spigot-Server-Patches/0385-improve-CraftWorld-isChunkLoaded.patch @@ -1,4 +1,4 @@ -From 62a24d634c1218c35dea6fae2583e2706974ab65 Mon Sep 17 00:00:00 2001 +From 90d05dc9e70d89f1d55c4f96b06311ab0a7e37a7 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 21 May 2019 02:34:04 +0100 Subject: [PATCH] improve CraftWorld#isChunkLoaded @@ -9,10 +9,10 @@ waiting for the execution queue to get to our request; We can just query the chunk status and get a response now, vs having to wait diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 447b4324f..3d62debc7 100644 +index 91b65fde05..b6ca8a9e94 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -401,14 +401,13 @@ public class CraftWorld implements World { +@@ -400,14 +400,13 @@ public class CraftWorld implements World { @Override public boolean isChunkLoaded(int x, int z) { @@ -30,5 +30,5 @@ index 447b4324f..3d62debc7 100644 throw new RuntimeException(ex); } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0387-Configurable-Keep-Spawn-Loaded-range-per-world.patch b/Spigot-Server-Patches/0386-Configurable-Keep-Spawn-Loaded-range-per-world.patch similarity index 96% rename from Spigot-Server-Patches/0387-Configurable-Keep-Spawn-Loaded-range-per-world.patch rename to Spigot-Server-Patches/0386-Configurable-Keep-Spawn-Loaded-range-per-world.patch index 0e97cce87d..33ab22446a 100644 --- a/Spigot-Server-Patches/0387-Configurable-Keep-Spawn-Loaded-range-per-world.patch +++ b/Spigot-Server-Patches/0386-Configurable-Keep-Spawn-Loaded-range-per-world.patch @@ -1,4 +1,4 @@ -From 334132e026377b7943f03223c055517f102a83cb Mon Sep 17 00:00:00 2001 +From 1f10b9fd531350980dd9ff08281228be6f16f939 Mon Sep 17 00:00:00 2001 From: Aikar Date: Sat, 13 Sep 2014 23:14:43 -0400 Subject: [PATCH] Configurable Keep Spawn Loaded range per world @@ -6,7 +6,7 @@ Subject: [PATCH] Configurable Keep Spawn Loaded range per world This lets you disable it for some worlds and lower it for others. diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java -index d8bb13693..de11a91af 100644 +index d8bb13693d..de11a91af6 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -483,4 +483,10 @@ public class PaperWorldConfig { @@ -21,7 +21,7 @@ index d8bb13693..de11a91af 100644 + } } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index c70ab3caf..c58f6f50d 100644 +index c70ab3caf0..c58f6f50d3 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -577,6 +577,13 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant Date: Fri, 24 May 2019 07:53:16 +0100 Subject: [PATCH] Fix some generation concurrency issues @@ -209,5 +209,5 @@ index ddf7268676..c2188ceef1 100644 } } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0389-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch b/Spigot-Server-Patches/0388-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch similarity index 92% rename from Spigot-Server-Patches/0389-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch rename to Spigot-Server-Patches/0388-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch index b43efdb6c3..99d5655ffa 100644 --- a/Spigot-Server-Patches/0389-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch +++ b/Spigot-Server-Patches/0388-MC-114618-Fix-EntityAreaEffectCloud-from-going-negat.patch @@ -1,4 +1,4 @@ -From 7714e202d48c4aaed00476736342817ff5ed8484 Mon Sep 17 00:00:00 2001 +From 4eadaf6901010d0f29d2a1e8705c28a07d454e8b Mon Sep 17 00:00:00 2001 From: William Blake Galbreath Date: Mon, 27 May 2019 17:35:39 -0500 Subject: [PATCH] MC-114618 - Fix EntityAreaEffectCloud from going negative @@ -23,5 +23,5 @@ index 3a8e105336..fe527aba52 100644 if (this.world.isClientSide) { ParticleParam particleparam = this.getParticle(); -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0390-ChunkMapDistance-CME.patch b/Spigot-Server-Patches/0389-ChunkMapDistance-CME.patch similarity index 97% rename from Spigot-Server-Patches/0390-ChunkMapDistance-CME.patch rename to Spigot-Server-Patches/0389-ChunkMapDistance-CME.patch index cd2097813d..23bd3daaa8 100644 --- a/Spigot-Server-Patches/0390-ChunkMapDistance-CME.patch +++ b/Spigot-Server-Patches/0389-ChunkMapDistance-CME.patch @@ -1,4 +1,4 @@ -From a80c7dd8d8dde293eabadf950661cc3f5b5d4fca Mon Sep 17 00:00:00 2001 +From 1536a61c14cc5fe7df1f034bf0f64eaabb8bb1f8 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 29 May 2019 04:01:22 +0100 Subject: [PATCH] ChunkMapDistance CME @@ -50,5 +50,5 @@ index 101eb58ace..63a688725e 100644 } else { if (!this.l.isEmpty()) { -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0391-Actually-Limit-Natural-Spawns-To-Limit.patch b/Spigot-Server-Patches/0390-Actually-Limit-Natural-Spawns-To-Limit.patch similarity index 98% rename from Spigot-Server-Patches/0391-Actually-Limit-Natural-Spawns-To-Limit.patch rename to Spigot-Server-Patches/0390-Actually-Limit-Natural-Spawns-To-Limit.patch index 03ee7aaf88..d87fbb2f96 100644 --- a/Spigot-Server-Patches/0391-Actually-Limit-Natural-Spawns-To-Limit.patch +++ b/Spigot-Server-Patches/0390-Actually-Limit-Natural-Spawns-To-Limit.patch @@ -1,4 +1,4 @@ -From cfd6e121847944f05ffba9e6650966f15db044a6 Mon Sep 17 00:00:00 2001 +From 23bfd69104bd51618d157c4cd09ba03441efddcf Mon Sep 17 00:00:00 2001 From: kickash32 Date: Sun, 2 Jun 2019 01:22:02 -0400 Subject: [PATCH] Actually-Limit-Natural-Spawns-To-Limit @@ -93,5 +93,5 @@ index c6ea37ffbd..5e6559df0b 100644 @Nullable -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0392-Implement-CraftBlockSoundGroup.patch b/Spigot-Server-Patches/0391-Implement-CraftBlockSoundGroup.patch similarity index 98% rename from Spigot-Server-Patches/0392-Implement-CraftBlockSoundGroup.patch rename to Spigot-Server-Patches/0391-Implement-CraftBlockSoundGroup.patch index 40aaa361dc..6005834d09 100644 --- a/Spigot-Server-Patches/0392-Implement-CraftBlockSoundGroup.patch +++ b/Spigot-Server-Patches/0391-Implement-CraftBlockSoundGroup.patch @@ -1,4 +1,4 @@ -From 282879b099576a7f5851f2306a9bfac9b6ac6f23 Mon Sep 17 00:00:00 2001 +From b85ae6b421d6b419d24f90744fbf0285dae533cd Mon Sep 17 00:00:00 2001 From: simpleauthority Date: Tue, 28 May 2019 03:48:51 -0700 Subject: [PATCH] Implement CraftBlockSoundGroup @@ -112,5 +112,5 @@ index 166c918d73..5296c6d9bf 100644 + // Paper end } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0393-Chunk-debug-command.patch b/Spigot-Server-Patches/0392-Chunk-debug-command.patch similarity index 99% rename from Spigot-Server-Patches/0393-Chunk-debug-command.patch rename to Spigot-Server-Patches/0392-Chunk-debug-command.patch index 00f62b3005..ae0d66c795 100644 --- a/Spigot-Server-Patches/0393-Chunk-debug-command.patch +++ b/Spigot-Server-Patches/0392-Chunk-debug-command.patch @@ -1,4 +1,4 @@ -From 9867b160e76f323ac2623bae1440a178adb502f1 Mon Sep 17 00:00:00 2001 +From 379ac9e206a7ea7e19c06067df3a8310b0969818 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 1 Jun 2019 13:00:55 -0700 Subject: [PATCH] Chunk debug command @@ -32,7 +32,7 @@ https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528273&page=com.atlass 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/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java -index 391726d99c..8db92edc36 100644 +index d704fc79c0..09efbf7250 100644 --- a/src/main/java/com/destroystokyo/paper/PaperCommand.java +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -28,14 +28,14 @@ public class PaperCommand extends Command { @@ -457,5 +457,5 @@ index 0430ca5353..badbe6c19d 100644 return this.b; } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0394-incremental-chunk-saving.patch b/Spigot-Server-Patches/0393-incremental-chunk-saving.patch similarity index 98% rename from Spigot-Server-Patches/0394-incremental-chunk-saving.patch rename to Spigot-Server-Patches/0393-incremental-chunk-saving.patch index 728d7b2801..665864863c 100644 --- a/Spigot-Server-Patches/0394-incremental-chunk-saving.patch +++ b/Spigot-Server-Patches/0393-incremental-chunk-saving.patch @@ -1,4 +1,4 @@ -From 24b13d1473de60e0424b5a62f4f92ffc471ebfc7 Mon Sep 17 00:00:00 2001 +From 2e3ff1fe99c5b8551dd4f9da59b0fc482f301d48 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 9 Jun 2019 03:53:22 +0100 Subject: [PATCH] incremental chunk saving @@ -147,7 +147,7 @@ index 493770bf68..2be6fa0f07 100644 if (flag) { List list = (List) this.visibleChunks.values().stream().filter(PlayerChunk::hasBeenLoaded).peek(PlayerChunk::m).collect(Collectors.toList()); diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java -index 1003ea50d3..4148325a26 100644 +index 8ac49d8b91..9fd14b573e 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -756,11 +756,44 @@ public class WorldServer extends World { @@ -197,5 +197,5 @@ index 1003ea50d3..4148325a26 100644 if (iprogressupdate != null) { iprogressupdate.a(new ChatMessage("menu.savingLevel", new Object[0])); -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0395-Catch-exceptions-from-dispenser-entity-spawns.patch b/Spigot-Server-Patches/0394-Catch-exceptions-from-dispenser-entity-spawns.patch similarity index 94% rename from Spigot-Server-Patches/0395-Catch-exceptions-from-dispenser-entity-spawns.patch rename to Spigot-Server-Patches/0394-Catch-exceptions-from-dispenser-entity-spawns.patch index 81a1790f75..89f8bf5923 100644 --- a/Spigot-Server-Patches/0395-Catch-exceptions-from-dispenser-entity-spawns.patch +++ b/Spigot-Server-Patches/0394-Catch-exceptions-from-dispenser-entity-spawns.patch @@ -1,4 +1,4 @@ -From 44d0cac4322b4f99b41506189e942406b720ffaf Mon Sep 17 00:00:00 2001 +From 2ed06dd3efd74e5b1062640643ebca2956e731eb Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Mon, 10 Jun 2019 09:36:40 +0100 Subject: [PATCH] Catch exceptions from dispenser entity spawns @@ -24,5 +24,5 @@ index 976c72208f..fe3d9d5fa3 100644 // CraftBukkit end return itemstack; -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0396-Fix-World-isChunkGenerated-calls.patch b/Spigot-Server-Patches/0395-Fix-World-isChunkGenerated-calls.patch similarity index 84% rename from Spigot-Server-Patches/0396-Fix-World-isChunkGenerated-calls.patch rename to Spigot-Server-Patches/0395-Fix-World-isChunkGenerated-calls.patch index 887f016f45..35cbc38961 100644 --- a/Spigot-Server-Patches/0396-Fix-World-isChunkGenerated-calls.patch +++ b/Spigot-Server-Patches/0395-Fix-World-isChunkGenerated-calls.patch @@ -1,4 +1,4 @@ -From ab24b40041486e74bcff020d9e040ae3706915c8 Mon Sep 17 00:00:00 2001 +From a35d57100af34be228d70a78e783305a54566e66 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 15 Jun 2019 08:54:33 -0700 Subject: [PATCH] Fix World#isChunkGenerated calls @@ -8,7 +8,7 @@ This patch also adds a chunk status cache on region files (note that its only purpose is to cache the status on DISK) diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java -index ca5963b11..3894b0434 100644 +index ca5963b11a..3894b04342 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -28,7 +28,7 @@ public class ChunkProviderServer extends IChunkProvider { @@ -43,7 +43,7 @@ index ca5963b11..3894b0434 100644 @Nullable diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java -index e778c2e85..73f93e494 100644 +index e778c2e857..73f93e4948 100644 --- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java +++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java @@ -410,6 +410,17 @@ public class ChunkRegionLoader { @@ -65,7 +65,7 @@ index e778c2e85..73f93e494 100644 if (nbttagcompound != null) { ChunkStatus chunkstatus = ChunkStatus.a(nbttagcompound.getCompound("Level").getString("Status")); diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java -index dd1822d6f..e324989b4 100644 +index dd1822d6ff..e324989b46 100644 --- a/src/main/java/net/minecraft/server/ChunkStatus.java +++ b/src/main/java/net/minecraft/server/ChunkStatus.java @@ -176,6 +176,7 @@ public class ChunkStatus { @@ -95,7 +95,7 @@ index dd1822d6f..e324989b4 100644 return (ChunkStatus) IRegistry.CHUNK_STATUS.get(MinecraftKey.a(s)); } diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java -index 14a176d61..98590e233 100644 +index 14a176d61d..98590e233a 100644 --- a/src/main/java/net/minecraft/server/PlayerChunk.java +++ b/src/main/java/net/minecraft/server/PlayerChunk.java @@ -70,6 +70,19 @@ public class PlayerChunk { @@ -119,7 +119,7 @@ index 14a176d61..98590e233 100644 public CompletableFuture> getStatusFutureUnchecked(ChunkStatus chunkstatus) { diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java -index 2be6fa0f0..bdadbd436 100644 +index 2be6fa0f07..bdadbd436e 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -891,11 +891,61 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { @@ -187,7 +187,7 @@ index 2be6fa0f0..bdadbd436 100644 boolean isOutsideOfRange(ChunkCoordIntPair chunkcoordintpair) { // Spigot start diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java -index ccc3d6c7a..b487e8060 100644 +index ccc3d6c7ad..b487e80602 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -31,6 +31,30 @@ public class RegionFile implements AutoCloseable { @@ -246,7 +246,7 @@ index ccc3d6c7a..b487e8060 100644 } diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java -index 6f34d8aea..d2b328945 100644 +index 6f34d8aea0..d2b3289450 100644 --- a/src/main/java/net/minecraft/server/RegionFileCache.java +++ b/src/main/java/net/minecraft/server/RegionFileCache.java @@ -47,6 +47,12 @@ public abstract class RegionFileCache implements AutoCloseable { @@ -279,10 +279,18 @@ index 6f34d8aea..d2b328945 100644 printOversizedLog("ChunkTooLarge even after reduction. Trying in overzealous mode.", regionfile.file, chunkX, chunkZ); // Eek, major fail. We have retry logic, so reduce threshholds and fall back diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 080f5abc1..f59b2e49c 100644 +index 3948de4674..20e9fd8a79 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -406,8 +406,22 @@ public class CraftWorld implements World { +@@ -18,6 +18,7 @@ import java.util.Objects; + import java.util.Random; + import java.util.Set; + import java.util.UUID; ++import java.util.concurrent.CompletableFuture; + import java.util.function.Predicate; + import it.unimi.dsi.fastutil.longs.Long2ObjectMap; + import it.unimi.dsi.fastutil.objects.ObjectSortedSet; +@@ -405,8 +406,22 @@ public class CraftWorld implements World { @Override public boolean isChunkGenerated(int x, int z) { @@ -306,7 +314,7 @@ index 080f5abc1..f59b2e49c 100644 } catch (IOException ex) { throw new RuntimeException(ex); } -@@ -519,20 +533,49 @@ public class CraftWorld implements World { +@@ -518,20 +533,49 @@ public class CraftWorld implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot @@ -365,63 +373,7 @@ index 080f5abc1..f59b2e49c 100644 + // Paper end } - @Override -@@ -2265,21 +2308,44 @@ public class CraftWorld implements World { - - // Paper start - private Chunk getChunkAtGen(int x, int z, boolean gen) { -- // copied from loadChunk() -- // this function is identical except we do not add a plugin ticket -- IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, gen || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); -+ // Note: Copied from loadChunk() -+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); - -- // If generate = false, but the chunk already exists, we will get this back. -- if (chunk instanceof ProtoChunkExtension) { -- // We then cycle through again to get the full chunk immediately, rather than after the ticket addition -- chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.FULL, true); -- } -+ if (!gen) { -+ -+ IChunkAccess immediate = world.getChunkProvider().getChunkAtImmediately(x, z); -+ if (immediate == null) { -+ immediate = world.getChunkProvider().playerChunkMap.getUnloadingChunk(x, z); -+ } -+ if (immediate != null) { -+ if (!(immediate instanceof ProtoChunkExtension) && !(immediate instanceof net.minecraft.server.Chunk)) { -+ return null; // not full status -+ } -+ return world.getChunkAt(x, z).bukkitChunk; // make sure we're at ticket level 33 or lower -+ } -+ -+ net.minecraft.server.RegionFile file; -+ try { -+ file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); -+ } catch (IOException ex) { -+ throw new RuntimeException(ex); -+ } -+ -+ ChunkStatus status = file.getStatusIfCached(x, z); -+ if (!file.chunkExists(chunkPos) || (status != null && status != ChunkStatus.FULL)) { -+ return null; -+ } -+ -+ IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.EMPTY, true); -+ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { -+ return null; -+ } - -- if (chunk instanceof net.minecraft.server.Chunk) { -- return ((net.minecraft.server.Chunk)chunk).bukkitChunk; -+ // fall through to load -+ // we load at empty so we don't double-load chunk data in this case - } - -- return null; -+ return world.getChunkAt(x, z).bukkitChunk; - } - @Override -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0397-Show-blockstate-location-if-we-failed-to-read-it.patch b/Spigot-Server-Patches/0396-Show-blockstate-location-if-we-failed-to-read-it.patch similarity index 95% rename from Spigot-Server-Patches/0397-Show-blockstate-location-if-we-failed-to-read-it.patch rename to Spigot-Server-Patches/0396-Show-blockstate-location-if-we-failed-to-read-it.patch index 2be332627e..60804ef28d 100644 --- a/Spigot-Server-Patches/0397-Show-blockstate-location-if-we-failed-to-read-it.patch +++ b/Spigot-Server-Patches/0396-Show-blockstate-location-if-we-failed-to-read-it.patch @@ -1,4 +1,4 @@ -From d06b36e7d5bd3ef23b800fce2c461581d1d8c0b2 Mon Sep 17 00:00:00 2001 +From cdadcf34bc202a34c0cde735f535a486f785a773 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 15 Jun 2019 10:28:25 -0700 Subject: [PATCH] Show blockstate location if we failed to read it @@ -33,5 +33,5 @@ index f6401e2cde..3e22d558ea 100644 public final boolean snapshotDisabled; // Paper -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0398-Log-other-thread-in-DataPaletteBlock-lock-failure.patch b/Spigot-Server-Patches/0397-Log-other-thread-in-DataPaletteBlock-lock-failure.patch similarity index 97% rename from Spigot-Server-Patches/0398-Log-other-thread-in-DataPaletteBlock-lock-failure.patch rename to Spigot-Server-Patches/0397-Log-other-thread-in-DataPaletteBlock-lock-failure.patch index 58d5711f75..0f6290f159 100644 --- a/Spigot-Server-Patches/0398-Log-other-thread-in-DataPaletteBlock-lock-failure.patch +++ b/Spigot-Server-Patches/0397-Log-other-thread-in-DataPaletteBlock-lock-failure.patch @@ -1,4 +1,4 @@ -From 3aa0ab156ffd994ad737bc2f76af3d7475c8a9b0 Mon Sep 17 00:00:00 2001 +From 0d67dccc3a8bf7bea2d579455ac07251f03f2472 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Fri, 21 Jun 2019 14:42:48 -0700 Subject: [PATCH] Log other thread in DataPaletteBlock lock failure @@ -47,5 +47,5 @@ index a3bb2e8779..1e2bca1e04 100644 crashreportsystemdetails.a("Thread dumps", (Object) s); -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0399-Use-ChunkStatus-cache-when-saving-protochunks.patch b/Spigot-Server-Patches/0398-Use-ChunkStatus-cache-when-saving-protochunks.patch similarity index 94% rename from Spigot-Server-Patches/0399-Use-ChunkStatus-cache-when-saving-protochunks.patch rename to Spigot-Server-Patches/0398-Use-ChunkStatus-cache-when-saving-protochunks.patch index d0047077aa..8918312b93 100644 --- a/Spigot-Server-Patches/0399-Use-ChunkStatus-cache-when-saving-protochunks.patch +++ b/Spigot-Server-Patches/0398-Use-ChunkStatus-cache-when-saving-protochunks.patch @@ -1,4 +1,4 @@ -From ced736fc443de0c3b20f67e4781b14aa63ecf6f9 Mon Sep 17 00:00:00 2001 +From cbcd19008d052efff2a8e4f04de3e0f16c92a455 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 22 Jun 2019 04:20:47 -0700 Subject: [PATCH] Use ChunkStatus cache when saving protochunks @@ -24,5 +24,5 @@ index bdadbd436e..fbbd4d5dd0 100644 } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0400-Anti-Xray.patch b/Spigot-Server-Patches/0399-Anti-Xray.patch similarity index 99% rename from Spigot-Server-Patches/0400-Anti-Xray.patch rename to Spigot-Server-Patches/0399-Anti-Xray.patch index 190d906a33..9d3a9e7177 100644 --- a/Spigot-Server-Patches/0400-Anti-Xray.patch +++ b/Spigot-Server-Patches/0399-Anti-Xray.patch @@ -1,4 +1,4 @@ -From 30e17b2ad5a927f202ccb708ab7d6d251db6b5eb Mon Sep 17 00:00:00 2001 +From cb6f6fe1c447df7fd2155cced33591e2dd410ae9 Mon Sep 17 00:00:00 2001 From: stonar96 Date: Mon, 20 Aug 2018 03:03:58 +0200 Subject: [PATCH] Anti-Xray @@ -1725,5 +1725,5 @@ index 7772d59005..4570ed9991 100644 return section; } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0401-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch b/Spigot-Server-Patches/0400-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch similarity index 96% rename from Spigot-Server-Patches/0401-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch rename to Spigot-Server-Patches/0400-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch index aab8b77169..0ec731d77b 100644 --- a/Spigot-Server-Patches/0401-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch +++ b/Spigot-Server-Patches/0400-Only-count-Natural-Spawned-mobs-towards-natural-spaw.patch @@ -1,4 +1,4 @@ -From bd8d126fa7f77f55f6e29c10fcc77390f250e282 Mon Sep 17 00:00:00 2001 +From fb5b05d9cbdd15c42b12c81e7444a6794728ac33 Mon Sep 17 00:00:00 2001 From: Aikar Date: Sun, 24 Mar 2019 01:01:32 -0400 Subject: [PATCH] Only count Natural Spawned mobs towards natural spawn mob @@ -38,7 +38,7 @@ index 929f5c3031..ff520d9e86 100644 public boolean asynchronous; public EngineMode engineMode; diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java -index 4148325a26..7faa5dd84a 100644 +index 9fd14b573e..1e5b15c2e2 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -899,6 +899,13 @@ public class WorldServer extends World { @@ -56,5 +56,5 @@ index 4148325a26..7faa5dd84a 100644 } } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0402-Use-getChunkIfLoadedImmediately-in-places.patch b/Spigot-Server-Patches/0401-Use-getChunkIfLoadedImmediately-in-places.patch similarity index 98% rename from Spigot-Server-Patches/0402-Use-getChunkIfLoadedImmediately-in-places.patch rename to Spigot-Server-Patches/0401-Use-getChunkIfLoadedImmediately-in-places.patch index 21d507697a..1284677e4e 100644 --- a/Spigot-Server-Patches/0402-Use-getChunkIfLoadedImmediately-in-places.patch +++ b/Spigot-Server-Patches/0401-Use-getChunkIfLoadedImmediately-in-places.patch @@ -1,4 +1,4 @@ -From 15ae33fdf438a0d55998add482f6964989fd98fa Mon Sep 17 00:00:00 2001 +From 3c29bd01d31049fe243a948921c2c00a1dbeb13d Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 8 Jul 2019 00:13:36 -0700 Subject: [PATCH] Use getChunkIfLoadedImmediately in places @@ -79,5 +79,5 @@ index f86404f83a..92601c581c 100644 } } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0403-Configurable-projectile-relative-velocity.patch b/Spigot-Server-Patches/0402-Configurable-projectile-relative-velocity.patch similarity index 98% rename from Spigot-Server-Patches/0403-Configurable-projectile-relative-velocity.patch rename to Spigot-Server-Patches/0402-Configurable-projectile-relative-velocity.patch index 8550a75dec..c71ae1e91c 100644 --- a/Spigot-Server-Patches/0403-Configurable-projectile-relative-velocity.patch +++ b/Spigot-Server-Patches/0402-Configurable-projectile-relative-velocity.patch @@ -1,4 +1,4 @@ -From d96a4efc6bb4e9f5337a315866a58004213d3c6b Mon Sep 17 00:00:00 2001 +From d8a7240ae87dc130e4e1db3cf1e65e79d9da2a88 Mon Sep 17 00:00:00 2001 From: Lucavon Date: Tue, 23 Jul 2019 20:29:20 -0500 Subject: [PATCH] Configurable projectile relative velocity @@ -65,5 +65,5 @@ index 18d28a151a..bd4ca73f6d 100644 @Override -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0404-Mark-entities-as-being-ticked-when-notifying-navigat.patch b/Spigot-Server-Patches/0403-Mark-entities-as-being-ticked-when-notifying-navigat.patch similarity index 93% rename from Spigot-Server-Patches/0404-Mark-entities-as-being-ticked-when-notifying-navigat.patch rename to Spigot-Server-Patches/0403-Mark-entities-as-being-ticked-when-notifying-navigat.patch index 24f64d14ff..65a2a96a84 100644 --- a/Spigot-Server-Patches/0404-Mark-entities-as-being-ticked-when-notifying-navigat.patch +++ b/Spigot-Server-Patches/0403-Mark-entities-as-being-ticked-when-notifying-navigat.patch @@ -1,4 +1,4 @@ -From c9ebdc9e0b94a5c9a4f56cd940ce9c99ac7230e5 Mon Sep 17 00:00:00 2001 +From 4ed947b04eb34215ca7c2f22bd492213348a01fd Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 28 Jul 2019 00:51:11 +0100 Subject: [PATCH] Mark entities as being ticked when notifying navigation @@ -25,5 +25,5 @@ index 1e5b15c2e2..84c16e2750 100644 } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0405-offset-item-frame-ticking.patch b/Spigot-Server-Patches/0404-offset-item-frame-ticking.patch similarity index 91% rename from Spigot-Server-Patches/0405-offset-item-frame-ticking.patch rename to Spigot-Server-Patches/0404-offset-item-frame-ticking.patch index 4766f205d4..18206c2ee1 100644 --- a/Spigot-Server-Patches/0405-offset-item-frame-ticking.patch +++ b/Spigot-Server-Patches/0404-offset-item-frame-ticking.patch @@ -1,4 +1,4 @@ -From 6571748607bac393655db682f7cb6621e8bac280 Mon Sep 17 00:00:00 2001 +From e7e0f94ce68757ad43c38a7f262757b0d885ad0c Mon Sep 17 00:00:00 2001 From: kickash32 Date: Tue, 30 Jul 2019 03:17:16 +0500 Subject: [PATCH] offset item frame ticking @@ -18,5 +18,5 @@ index 3b282a18a2..2b4a849f48 100644 protected EnumDirection direction; -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0406-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch b/Spigot-Server-Patches/0405-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch similarity index 89% rename from Spigot-Server-Patches/0406-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch rename to Spigot-Server-Patches/0405-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch index 8acc586562..e406d54da2 100644 --- a/Spigot-Server-Patches/0406-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch +++ b/Spigot-Server-Patches/0405-Preserve-old-flush-on-save-flag-for-reliable-regionf.patch @@ -1,4 +1,4 @@ -From 0739db695afc0dc5c7e9f6dbe6289075455cadee Mon Sep 17 00:00:00 2001 +From 4e9e8fcb96e24d84f1ea01726960e000e0747fda Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 5 Aug 2019 08:24:01 -0700 Subject: [PATCH] Preserve old flush on save flag for reliable regionfiles @@ -6,7 +6,7 @@ Subject: [PATCH] Preserve old flush on save flag for reliable regionfiles Originally this patch was in paper diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java -index e6e412b7c1..b4c191d538 100644 +index b487e80602..a8c8ace46c 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -349,7 +349,7 @@ public class RegionFile implements AutoCloseable { @@ -19,5 +19,5 @@ index e6e412b7c1..b4c191d538 100644 if (!FLUSH_ON_SAVE) { return; -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0407-Avoid-hopper-searches-if-there-are-no-items.patch b/Spigot-Server-Patches/0406-Avoid-hopper-searches-if-there-are-no-items.patch similarity index 97% rename from Spigot-Server-Patches/0407-Avoid-hopper-searches-if-there-are-no-items.patch rename to Spigot-Server-Patches/0406-Avoid-hopper-searches-if-there-are-no-items.patch index 0ea5e94fd3..6eb12c1559 100644 --- a/Spigot-Server-Patches/0407-Avoid-hopper-searches-if-there-are-no-items.patch +++ b/Spigot-Server-Patches/0406-Avoid-hopper-searches-if-there-are-no-items.patch @@ -1,4 +1,4 @@ -From a788e2b42ef19a7ef34ee11da916bdf578a8eb9a Mon Sep 17 00:00:00 2001 +From 930f77a8a2f97b5969f057112520bf94df821400 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 @@ -14,7 +14,7 @@ And since minecart hoppers are used _very_ rarely near we can avoid alot of sear Combined, this adds up a lot. diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java -index d604f96c1..67dc837f4 100644 +index d604f96c16..67dc837f43 100644 --- a/src/main/java/net/minecraft/server/Chunk.java +++ b/src/main/java/net/minecraft/server/Chunk.java @@ -84,6 +84,10 @@ public class Chunk implements IChunkAccess { @@ -90,7 +90,7 @@ index d604f96c1..67dc837f4 100644 while (iterator.hasNext()) { diff --git a/src/main/java/net/minecraft/server/IEntitySelector.java b/src/main/java/net/minecraft/server/IEntitySelector.java -index 56488b78d..56739e6ed 100644 +index 56488b78dd..56739e6ed5 100644 --- a/src/main/java/net/minecraft/server/IEntitySelector.java +++ b/src/main/java/net/minecraft/server/IEntitySelector.java @@ -11,6 +11,7 @@ public final class IEntitySelector { @@ -102,5 +102,5 @@ index 56488b78d..56739e6ed 100644 return entity instanceof IInventory && entity.isAlive(); }; -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0408-Fixed-MC-156852.patch b/Spigot-Server-Patches/0407-Fixed-MC-156852.patch similarity index 92% rename from Spigot-Server-Patches/0408-Fixed-MC-156852.patch rename to Spigot-Server-Patches/0407-Fixed-MC-156852.patch index 519a64648a..27e60ae4db 100644 --- a/Spigot-Server-Patches/0408-Fixed-MC-156852.patch +++ b/Spigot-Server-Patches/0407-Fixed-MC-156852.patch @@ -1,4 +1,4 @@ -From 6e3e4c0d0ef13a74817402992dc91eeb14611941 Mon Sep 17 00:00:00 2001 +From a3e706da03ede31d3d5f297acb569baad24a7803 Mon Sep 17 00:00:00 2001 From: TheGreatKetchup Date: Thu, 1 Aug 2019 21:24:30 -0400 Subject: [PATCH] Fixed MC-156852 @@ -12,7 +12,7 @@ issue in 1.8-1.12. Originally solved by Gnembon on MC-5694 at bugs.mojang.com diff --git a/src/main/java/net/minecraft/server/PlayerInteractManager.java b/src/main/java/net/minecraft/server/PlayerInteractManager.java -index e5e9de542..c96564a59 100644 +index e5e9de542b..c96564a59b 100644 --- a/src/main/java/net/minecraft/server/PlayerInteractManager.java +++ b/src/main/java/net/minecraft/server/PlayerInteractManager.java @@ -218,6 +218,7 @@ public class PlayerInteractManager { @@ -24,5 +24,5 @@ index e5e9de542..c96564a59 100644 this.l = j; } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0409-Implement-alternative-item-despawn-rate.patch b/Spigot-Server-Patches/0408-Implement-alternative-item-despawn-rate.patch similarity index 97% rename from Spigot-Server-Patches/0409-Implement-alternative-item-despawn-rate.patch rename to Spigot-Server-Patches/0408-Implement-alternative-item-despawn-rate.patch index 04a7a9d585..d3dc7c5cb4 100644 --- a/Spigot-Server-Patches/0409-Implement-alternative-item-despawn-rate.patch +++ b/Spigot-Server-Patches/0408-Implement-alternative-item-despawn-rate.patch @@ -1,11 +1,11 @@ -From 5356f6eb7d8023a1e71837b544ef89f745274aaf Mon Sep 17 00:00:00 2001 +From 62f3bf54c31e04625bbf93b067e1f542f3c6f6e4 Mon Sep 17 00:00:00 2001 From: kickash32 Date: Mon, 3 Jun 2019 02:02:39 -0400 Subject: [PATCH] Implement alternative item-despawn-rate diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java -index 318a470ee..e7bbeef74 100644 +index 318a470eea..e7bbeef74d 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -1,12 +1,17 @@ @@ -80,7 +80,7 @@ index 318a470ee..e7bbeef74 100644 + } } diff --git a/src/main/java/net/minecraft/server/EntityItem.java b/src/main/java/net/minecraft/server/EntityItem.java -index 209169895..97e379090 100644 +index 2091698953..97e3790908 100644 --- a/src/main/java/net/minecraft/server/EntityItem.java +++ b/src/main/java/net/minecraft/server/EntityItem.java @@ -5,6 +5,7 @@ import java.util.List; @@ -128,5 +128,5 @@ index 209169895..97e379090 100644 public Packet N() { return new PacketPlayOutSpawnEntity(this); -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0410-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch b/Spigot-Server-Patches/0409-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch similarity index 95% rename from Spigot-Server-Patches/0410-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch rename to Spigot-Server-Patches/0409-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch index d9b0791e22..c605a6b1a9 100644 --- a/Spigot-Server-Patches/0410-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch +++ b/Spigot-Server-Patches/0409-Do-less-work-if-we-have-a-custom-Bukkit-generator.patch @@ -1,4 +1,4 @@ -From eb82ba85c2d5504bd270977a9708bd7fe621a1c7 Mon Sep 17 00:00:00 2001 +From d97061fb09c251d90f2388a3ad3ef3975b063dd8 Mon Sep 17 00:00:00 2001 From: Paul Sauve Date: Sun, 14 Jul 2019 21:05:03 -0500 Subject: [PATCH] Do less work if we have a custom Bukkit generator @@ -7,7 +7,7 @@ If the Bukkit generator already has a spawn, use it immediately instead of spending time generating one that we won't use diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java -index 451ad4f32..ad4e65e48 100644 +index 84c16e2750..4497f6a601 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -663,12 +663,6 @@ public class WorldServer extends World { @@ -39,5 +39,5 @@ index 451ad4f32..ad4e65e48 100644 WorldServer.LOGGER.warn("Unable to find spawn biome"); } -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0411-Fix-MC-158900.patch b/Spigot-Server-Patches/0410-Fix-MC-158900.patch similarity index 93% rename from Spigot-Server-Patches/0411-Fix-MC-158900.patch rename to Spigot-Server-Patches/0410-Fix-MC-158900.patch index dfa9f91589..7df8d78873 100644 --- a/Spigot-Server-Patches/0411-Fix-MC-158900.patch +++ b/Spigot-Server-Patches/0410-Fix-MC-158900.patch @@ -1,4 +1,4 @@ -From 798c94094b6037e2e6b5a50d4e2488e01365da71 Mon Sep 17 00:00:00 2001 +From 3925d94376e662aa8711754eb27b22df40a42c0a Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Tue, 13 Aug 2019 06:35:17 -0700 Subject: [PATCH] Fix MC-158900 @@ -7,7 +7,7 @@ The problem was we were checking isExpired() on the entry, but if it was expired at that point, then it would be null. diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java -index a183bb450d..bf37c215c3 100644 +index a183bb450d..3cb443c4ff 100644 --- a/src/main/java/net/minecraft/server/PlayerList.java +++ b/src/main/java/net/minecraft/server/PlayerList.java @@ -512,8 +512,10 @@ public abstract class PlayerList { @@ -24,5 +24,5 @@ index a183bb450d..bf37c215c3 100644 chatmessage = new ChatMessage("multiplayer.disconnect.banned.reason", new Object[]{gameprofilebanentry.getReason()}); if (gameprofilebanentry.getExpires() != null) { -- -2.22.0 +2.22.1 diff --git a/Spigot-Server-Patches/0411-Asynchronous-chunk-IO-and-loading.patch b/Spigot-Server-Patches/0411-Asynchronous-chunk-IO-and-loading.patch new file mode 100644 index 0000000000..5130727e64 --- /dev/null +++ b/Spigot-Server-Patches/0411-Asynchronous-chunk-IO-and-loading.patch @@ -0,0 +1,3825 @@ +From 3e8fc610d83ee7f81bb9107356df8b1d6c15bda4 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 + +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 92c32c48d2..f4d5db02f7 100644 +--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java ++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +@@ -58,6 +58,17 @@ public class WorldTimingsHandler { + public final Timing worldSaveLevel; + public final Timing chunkSaveData; + ++ public final Timing poiUnload; ++ public final Timing chunkUnload; ++ public final Timing poiSaveDataSerialization; ++ public final Timing chunkSave; ++ public final Timing chunkSaveOverwriteCheck; ++ public final Timing chunkSaveDataSerialization; ++ public final Timing chunkSaveIOWait; ++ public final Timing chunkUnloadPrepareSave; ++ public final Timing chunkUnloadPOISerialization; ++ public final Timing chunkUnloadDataSave; ++ + public WorldTimingsHandler(World server) { + String name = server.worldData.getName() +" - "; + +@@ -112,6 +123,17 @@ public class WorldTimingsHandler { + chunkProviderTick = Timings.ofSafe(name + "Chunk provider tick"); + broadcastChunkUpdates = Timings.ofSafe(name + "Broadcast chunk updates"); + countNaturalMobs = Timings.ofSafe(name + "Count natural mobs"); ++ ++ poiUnload = Timings.ofSafe(name + "Chunk unload - POI"); ++ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk"); ++ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization"); ++ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk"); ++ chunkSaveOverwriteCheck = Timings.ofSafe(name + "Chunk save - Chunk Overwrite Check"); ++ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization"); ++ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait"); ++ 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(WorldServer worldserver, String timingsType) { +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +index 5942c3438e..61eeb6747a 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -1,5 +1,6 @@ + package com.destroystokyo.paper; + ++import com.destroystokyo.paper.io.chunk.ChunkTaskManager; + import com.google.common.base.Strings; + import com.google.common.base.Throwables; + +@@ -388,4 +389,64 @@ public class PaperConfig { + maxBookPageSize = getInt("settings.book-size.page-max", maxBookPageSize); + maxBookTotalSizeMultiplier = getDouble("settings.book-size.total-multiplier", maxBookTotalSizeMultiplier); + } ++ ++ public static boolean asyncChunks = false; ++ //public static boolean asyncChunkGeneration = true; // Leave out for now until we can control this ++ //public static boolean asyncChunkGenThreadPerWorld = true; // Leave out for now until we can control this ++ public static int asyncChunkLoadThreads = -1; ++ private static void asyncChunks() { ++ if (version < 15) { ++ boolean enabled = config.getBoolean("settings.async-chunks", true); ++ ConfigurationSection section = config.createSection("settings.async-chunks"); ++ section.set("enable", enabled); ++ section.set("load-threads", -1); ++ section.set("generation", true); ++ section.set("thread-per-world-generation", true); ++ } ++ ++ // TODO load threads now control async chunk save for unloading chunks, look into renaming this? ++ ++ asyncChunks = getBoolean("settings.async-chunks.enable", true); ++ //asyncChunkGeneration = getBoolean("settings.async-chunks.generation", true); // Leave out for now until we can control this ++ //asyncChunkGenThreadPerWorld = getBoolean("settings.async-chunks.thread-per-world-generation", true); // Leave out for now until we can control this ++ asyncChunkLoadThreads = getInt("settings.async-chunks.load-threads", -1); ++ if (asyncChunkLoadThreads <= 0) { ++ asyncChunkLoadThreads = (int) Math.min(Integer.getInteger("paper.maxChunkThreads", 8), Math.max(1, Runtime.getRuntime().availableProcessors() - 1)); ++ } ++ ++ // Let Shared Host set some limits ++ String sharedHostEnvGen = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_GEN"); ++ String sharedHostEnvLoad = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_LOAD"); ++ /* Ignore temporarily - we cannot control the gen threads (for now) ++ if ("1".equals(sharedHostEnvGen)) { ++ log("Async Chunks - Generation: Your host has requested to use a single thread world generation"); ++ asyncChunkGenThreadPerWorld = false; ++ } else if ("2".equals(sharedHostEnvGen)) { ++ log("Async Chunks - Generation: Your host has disabled async world generation - You will experience lag from world generation"); ++ asyncChunkGeneration = false; ++ } ++ */ ++ ++ if (sharedHostEnvLoad != null) { ++ try { ++ asyncChunkLoadThreads = Math.max(1, Math.min(asyncChunkLoadThreads, Integer.parseInt(sharedHostEnvLoad))); ++ } catch (NumberFormatException ignored) {} ++ } ++ ++ if (!asyncChunks) { ++ log("Async Chunks: Disabled - Chunks will be managed synchronosuly, and will cause tremendous lag."); ++ } else { ++ ChunkTaskManager.initGlobalLoadThreads(asyncChunkLoadThreads); ++ log("Async Chunks: Enabled - Chunks will be loaded much faster, without lag."); ++ /* Ignore temporarily - we cannot control the gen threads (for now) ++ if (!asyncChunkGeneration) { ++ log("Async Chunks - Generation: Disabled - Chunks will be generated synchronosuly, and will cause tremendous lag."); ++ } else if (asyncChunkGenThreadPerWorld) { ++ log("Async Chunks - Generation: Enabled - Chunks will be generated much faster, without lag."); ++ } else { ++ log("Async Chunks - Generation: Enabled (Single Thread) - Chunks will be generated much faster, without lag."); ++ } ++ */ ++ } ++ } + } +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +index 23626bef3a..1edcecd2ee 100644 +--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -9,6 +9,7 @@ import java.util.concurrent.Executors; + import java.util.concurrent.atomic.AtomicInteger; + import java.util.function.Supplier; + ++import com.destroystokyo.paper.io.PrioritizedTaskQueue; + import net.minecraft.server.*; + import org.bukkit.Bukkit; + import org.bukkit.World.Environment; +@@ -150,6 +151,12 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + + private final AtomicInteger xrayRequests = new AtomicInteger(); + ++ // Paper start - async chunk api ++ private Integer nextTicketHold() { ++ return Integer.valueOf(this.xrayRequests.getAndIncrement()); ++ } ++ // Paper end ++ + private Integer addXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider) { + final Integer hold = Integer.valueOf(this.xrayRequests.getAndIncrement()); + +@@ -181,6 +188,35 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + chunk.world.getChunkAt(locX, locZ + 1); + } + ++ // Paper start - async chunk api ++ private void loadNeighbourAsync(ChunkProviderServer chunkProvider, WorldServer world, int chunkX, int chunkZ, int[] counter, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) { ++ chunkProvider.getChunkAtAsynchronously(chunkX, chunkZ, true, (Chunk neighbour) -> { ++ onNeighourLoad.accept(neighbour); ++ if (++counter[0] == 4) { ++ onAllNeighboursLoad.run(); ++ } ++ }); ++ world.asyncChunkTaskManager.raisePriority(chunkX, chunkZ, PrioritizedTaskQueue.HIGHER_PRIORITY); ++ } ++ ++ private void loadNeighboursAsync(Chunk chunk, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) { ++ int[] loaded = new int[1]; ++ ++ int locX = chunk.getPos().x; ++ int locZ = chunk.getPos().z; ++ WorldServer world = ((WorldServer)chunk.world); ++ ++ onNeighourLoad.accept(chunk); ++ ++ ChunkProviderServer chunkProvider = world.getChunkProvider(); ++ ++ this.loadNeighbourAsync(chunkProvider, world, locX - 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, world, locX + 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, world, locX, locZ - 1, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, world, locX, locZ + 1, loaded, onNeighourLoad, onAllNeighboursLoad); ++ } ++ // Paper end ++ + @Override + public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) { + int locX = chunk.getPos().x; +@@ -256,11 +292,15 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + + if (chunks[0] == null || chunks[1] == null || chunks[2] == null || chunks[3] == null) { + // we need to load +- MinecraftServer.getServer().scheduleOnMain(() -> { +- Integer ticketHold = this.addXrayTickets(locX, locZ, world.getChunkProvider()); +- this.loadNeighbours(chunk); ++ // Paper start - async chunk api ++ Integer ticketHold = this.nextTicketHold(); ++ this.loadNeighboursAsync(chunk, (Chunk neighbour) -> { // when a neighbour is loaded ++ ((WorldServer)neighbour.world).getChunkProvider().addTicket(TicketType.ANTIXRAY, neighbour.getPos(), 0, ticketHold); ++ }, ++ () -> { // once neighbours get loaded + this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, false, ticketHold); + }); ++ // Paper end + return; + } + +diff --git a/src/main/java/com/destroystokyo/paper/io/IOUtil.java b/src/main/java/com/destroystokyo/paper/io/IOUtil.java +new file mode 100644 +index 0000000000..5af0ac3d9e +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java +@@ -0,0 +1,62 @@ ++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 0000000000..4f10a8311e +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java +@@ -0,0 +1,661 @@ ++package com.destroystokyo.paper.io; ++ ++import net.minecraft.server.ChunkCoordIntPair; ++import net.minecraft.server.ExceptionWorldConflict; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.NBTTagCompound; ++import net.minecraft.server.RegionFile; ++import net.minecraft.server.WorldServer; ++import org.apache.logging.log4j.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(WorldServer, int, int, NBTTagCompound, NBTTagCompound, int) ++ * @see #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean) ++ */ ++public final class PaperFileIOThread extends QueueExecutorThread { ++ ++ public static final Logger LOGGER = MinecraftServer.LOGGER; ++ public static final NBTTagCompound FAILURE_VALUE = new NBTTagCompound(); ++ ++ 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.fatal("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 WorldServer 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); ++ } ++ } ++ ++ // Hack start ++ /** ++ * if {@code waitForRead} is true, then this task will wait on an available read task, else it will wait on an available ++ * write task ++ * if {@code poiTask} is true, then this task will wait on a poi task, else it will wait on chunk data task ++ * @deprecated API is garbage and will only work for main thread queueing of tasks (which is vanilla), plugins messing ++ * around asynchronously will give unexpected results ++ * @return whether the task succeeded, or {@code null} if there is no task ++ */ ++ @Deprecated ++ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, final boolean waitForRead, ++ final boolean poiTask) { ++ final ChunkDataTask task; ++ ++ final Long key = IOUtil.getCoordinateKey(chunkX, chunkZ); ++ if (poiTask) { ++ task = world.poiDataController.tasks.get(key); ++ } else { ++ task = world.chunkDataController.tasks.get(key); ++ } ++ ++ if (task == null) { ++ return null; ++ } ++ ++ if (waitForRead) { ++ ChunkDataController.InProgressRead read = task.inProgressRead; ++ if (read == null) { ++ return null; ++ } ++ return Boolean.valueOf(read.readFuture.join() != PaperFileIOThread.FAILURE_VALUE); ++ } ++ ++ // wait for write ++ ChunkDataController.InProgressWrite write = task.inProgressWrite; ++ if (write == null) { ++ return null; ++ } ++ return Boolean.valueOf(write.wrote.join() != PaperFileIOThread.FAILURE_VALUE); ++ } ++ // Hack end ++ ++ public NBTTagCompound getPendingWrite(final WorldServer world, final int chunkX, final int chunkZ, final boolean poiData) { ++ final ChunkDataController taskController = poiData ? world.poiDataController : world.chunkDataController; ++ ++ final ChunkDataTask dataTask = taskController.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ))); ++ ++ if (dataTask == null) { ++ return null; ++ } ++ ++ final ChunkDataController.InProgressWrite write = dataTask.inProgressWrite; ++ ++ if (write == null) { ++ return null; ++ } ++ ++ return write.data; ++ } ++ ++ /** ++ * Sets the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued. ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param priority Priority level to set to ++ */ ++ public void setPriority(final WorldServer 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 WorldServer world, final int chunkX, final int chunkZ, ++ final NBTTagCompound poiData, final NBTTagCompound 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 WorldServer world, ++ final int chunkX, final int chunkZ, final NBTTagCompound 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(WorldServer, 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 WorldServer 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 WorldServer 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 NBTTagCompound 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 NBTTagCompound 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 WorldServer 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.chunkExists(new ChunkCoordIntPair(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(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns ++ * the {@link ChunkData} associated with the specified chunk when the task is complete. ++ * @return The chunk data, or {@code null} if the chunk failed to load. ++ */ ++ public ChunkData loadChunkData(final WorldServer world, final int chunkX, final int chunkZ, final int priority, ++ 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.fatal("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable); ++ } ++ } ++ } ++ ++ public static final class ChunkData { ++ ++ public NBTTagCompound poiData; ++ public NBTTagCompound chunkData; ++ ++ public ChunkData() {} ++ ++ public ChunkData(final NBTTagCompound poiData, final NBTTagCompound 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 NBTTagCompound compound) throws IOException; ++ public abstract NBTTagCompound readData(final int x, final int z) throws IOException; ++ ++ public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function); ++ public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function); ++ ++ public static final class InProgressWrite { ++ public long writeCounter; ++ public NBTTagCompound data; ++ ++ // Hack start ++ @Deprecated ++ public CompletableFuture wrote = new CompletableFuture<>(); ++ // Hack end ++ } ++ ++ 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 WorldServer world; ++ private final int x; ++ private final int z; ++ private final ChunkDataController taskController; ++ ++ public ChunkDataTask(final int priority, final WorldServer 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.inProgressWrite.wrote = new CompletableFuture<>(); // Hack ++ this.priority.lazySet(priority); ++ PaperFileIOThread.Holder.INSTANCE.queueTask(this); ++ } ++ ++ @Override ++ public void run() { ++ ChunkDataController.InProgressRead read = this.inProgressRead; ++ if (read != null) { ++ NBTTagCompound 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.fatal("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; ++ } ++ ++ // check if another process is writing ++ try { ++ this.world.checkSession(); ++ } catch (final ExceptionWorldConflict ex) { ++ LOGGER.fatal("Couldn't save chunk; already in use by another instance of Minecraft?", ex); ++ // we don't need to set the write counter to -1 as we know at this stage there's no point in re-scheduling ++ // writes since they'll fail anyways. ++ write.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack - However we need to fail the write ++ return; ++ } ++ ++ for (;;) { ++ final long writeCounter; ++ final NBTTagCompound 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.fatal("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) { ++ ChunkDataTask.this.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ ChunkDataTask.this.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE); // Hack ++ 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; ++ valueInMap.inProgressWrite.wrote.complete(PaperFileIOThread.FAILURE_VALUE); ++ } else { ++ valueInMap.inProgressWrite.wrote.complete(data); ++ } ++ ++ 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 0000000000..c3ca3c4a1c +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java +@@ -0,0 +1,258 @@ ++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 { ++ task.onQueue(this); ++ this.queues[task.getPriority()].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; ++ } ++ ++ /** ++ * 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 onQueue(final PrioritizedTaskQueue queue) { ++ if (this.queue.getAndSet(queue) != null) { ++ throw new IllegalStateException("Already queued!"); ++ } ++ } ++ ++ /* 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 0000000000..f127ef236e +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java +@@ -0,0 +1,244 @@ ++package com.destroystokyo.paper.io; ++ ++import net.minecraft.server.MinecraftServer; ++import org.apache.logging.log4j.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 = MinecraftServer.LOGGER; ++ ++ protected final PrioritizedTaskQueue queue; ++ protected final long spinWaitTime; ++ ++ protected volatile boolean closed; ++ ++ protected final AtomicBoolean parked = new AtomicBoolean(); ++ ++ protected volatile ConcurrentLinkedQueue flushQueue = new ConcurrentLinkedQueue<>(); ++ ++ // this is required to synchronize LockSupport#park() ++ // LockSupport explicitly states that it will only follow ordering with respect to volatile access ++ // see flush() for more details ++ protected volatile long flushCounter; ++ ++ 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) ++ ++ // it also resolves race condition where we've overriden a concurrent thread's flush call which set parked to false ++ // the important ordering: (volatile guarantees we cannot re-order the below events) ++ // us: parked -> true, parse tasks -> writeCounter + 1 -> drain flush queue ++ // them: read write counter -> add to flush queue -> write parked to false -> park loop ++ ++ // if we overwrite their set parked to false call then they're in the park loop or about to be, and we're about to ++ // drain the flush queue ++ 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()) != null) { ++ ret = true; ++ try { ++ task.run(); ++ } catch (final Throwable throwable) { ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ LOGGER.fatal("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) { ++ final ConcurrentLinkedQueue flushQueue = this.flushQueue; // Note: this can be a plain read ++ if (shutdown) { ++ this.flushQueue = null; // Note: this can be a release write ++ } ++ ++ Thread current; ++ ++ while ((current = flushQueue.poll()) != null) { ++ this.pollTasks(false); ++ // increment flush counter so threads will wake up after being unparked() ++ //noinspection NonAtomicOperationOnVolatileField ++ ++this.flushCounter; // may be plain read plain write if we order before poll() (also would need to re-order pollTasks) ++ LockSupport.unpark(current); ++ } ++ } ++ ++ /** ++ * 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 ++ ++ long flushCounter = this.flushCounter; ++ ++ ConcurrentLinkedQueue flushQueue = this.flushQueue; ++ ++ // it's important to read the flush queue after the flush counter to ensure that if we proceed from here ++ // we have a flush counter that would be different from the final flush counter if the queue executor shuts down ++ // the double read of the flush queue is not enough to account for this since ++ if (flushQueue == null) { ++ return; // queue executor has received shutdown and emptied queue ++ } ++ ++ flushQueue.add(currentThread); ++ ++ // re-check null flush queue, we need to guarantee the executor is not shutting down before parking ++ ++ if (this.flushQueue == null) { ++ // cannot guarantee state of flush queue now, the executor is done though ++ return; ++ } ++ ++ // force a response from the IO thread, we're not sure of its state currently ++ this.parked.set(false); ++ LockSupport.unpark(this); ++ ++ // Note: see the run() function for handling of a race condition where the queue executor overwrites our parked write ++ ++ boolean interrupted = false; // preserve interrupted status ++ ++ while (this.flushCounter == flushCounter) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ ++ /** ++ * 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 0000000000..305da47868 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java +@@ -0,0 +1,149 @@ ++package com.destroystokyo.paper.io.chunk; ++ ++import co.aikar.timings.Timing; ++import com.destroystokyo.paper.io.PaperFileIOThread; ++import com.destroystokyo.paper.io.IOUtil; ++import net.minecraft.server.ChunkCoordIntPair; ++import net.minecraft.server.ChunkRegionLoader; ++import net.minecraft.server.PlayerChunkMap; ++import net.minecraft.server.WorldServer; ++ ++import java.util.ArrayDeque; ++import java.util.function.Consumer; ++ ++public final class ChunkLoadTask extends ChunkTask { ++ ++ public boolean cancelled; ++ ++ Consumer onComplete; ++ public PaperFileIOThread.ChunkData chunkData; ++ ++ private boolean hasCompleted; ++ ++ public ChunkLoadTask(final WorldServer 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 ChunkRegionLoader.InProgressChunkHolder createEmptyHolder() { ++ return new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE); ++ } ++ ++ @Override ++ public void run() { ++ try { ++ this.executeTask(); ++ } catch (final Throwable ex) { ++ PaperFileIOThread.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) { ++ PaperFileIOThread.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 ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(this.chunkX, this.chunkZ); ++ ++ final PlayerChunkMap chunkManager = this.world.getChunkProvider().playerChunkMap; ++ ++ try (Timing ignored = this.world.timings.chunkIOStage1.startTimingIfSync()) { ++ final ChunkRegionLoader.InProgressChunkHolder chunkHolder; ++ ++ // apply fixes ++ ++ try { ++ if (chunkData.poiData != null) { ++ chunkData.poiData = chunkData.poiData.clone(); // clone data for safety, file IO thread does not clone ++ } ++ chunkData.chunkData = chunkManager.getChunkData(this.world.getWorldProvider().getDimensionManager(), ++ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData.clone(), chunkPos, this.world); // clone data for safety, file IO thread does not clone ++ } catch (final Throwable ex) { ++ PaperFileIOThread.LOGGER.error("Could not apply datafixers for chunk task: " + this.toString(), ex); ++ this.complete(ChunkLoadTask.createEmptyHolder()); ++ } ++ ++ if (this.checkCancelled()) { ++ return; ++ } ++ ++ try { ++ this.world.getChunkProvider().playerChunkMap.updateChunkStatusOnDisk(chunkPos, chunkData.chunkData); ++ } catch (final Throwable ex) { ++ PaperFileIOThread.LOGGER.warn("Failed to update chunk status cache for task: " + this.toString(), ex); ++ // non-fatal, continue ++ } ++ ++ try { ++ chunkHolder = ChunkRegionLoader.loadChunk(this.world, ++ chunkManager.definedStructureManager, chunkManager.getVillagePlace(), chunkPos, ++ chunkData.chunkData, true); ++ } catch (final Throwable ex) { ++ PaperFileIOThread.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 ChunkRegionLoader.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) { ++ PaperFileIOThread.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 0000000000..60312b85f9 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java +@@ -0,0 +1,112 @@ ++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 net.minecraft.server.ChunkRegionLoader; ++import net.minecraft.server.IAsyncTaskHandler; ++import net.minecraft.server.IChunkAccess; ++import net.minecraft.server.NBTTagCompound; ++import net.minecraft.server.WorldServer; ++ ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.atomic.AtomicInteger; ++ ++public final class ChunkSaveTask extends ChunkTask { ++ ++ public final ChunkRegionLoader.AsyncSaveData asyncSaveData; ++ public final IChunkAccess chunk; ++ public final CompletableFuture onComplete = new CompletableFuture<>(); ++ ++ private final AtomicInteger attemptedPriority; ++ ++ public ChunkSaveTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority, ++ final ChunkTaskManager taskManager, final ChunkRegionLoader.AsyncSaveData asyncSaveData, ++ final IChunkAccess 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 NBTTagCompound compound; ++ ++ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTimingIfSync()) { ++ compound = ChunkRegionLoader.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()) { ++ NBTTagCompound data = PaperFileIOThread.FAILURE_VALUE; ++ ++ try { ++ data = ChunkRegionLoader.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.fatal("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 NBTTagCompound 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 0000000000..1dfa8abfd8 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java +@@ -0,0 +1,40 @@ ++package com.destroystokyo.paper.io.chunk; ++ ++import com.destroystokyo.paper.io.PaperFileIOThread; ++import com.destroystokyo.paper.io.PrioritizedTaskQueue; ++import net.minecraft.server.WorldServer; ++ ++abstract class ChunkTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable { ++ ++ public final WorldServer world; ++ public final int chunkX; ++ public final int chunkZ; ++ public final ChunkTaskManager taskManager; ++ ++ public ChunkTask(final WorldServer 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 0000000000..98a9744a0e +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java +@@ -0,0 +1,429 @@ ++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 net.minecraft.server.ChunkRegionLoader; ++import net.minecraft.server.IAsyncTaskHandler; ++import net.minecraft.server.IChunkAccess; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.NBTTagCompound; ++import net.minecraft.server.WorldServer; ++import org.apache.logging.log4j.Level; ++import org.bukkit.Bukkit; ++import org.spigotmc.AsyncCatcher; ++ ++import java.util.ArrayDeque; ++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 WorldServer 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); ++ ++ // 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 WorldServer world; ++ ++ public ChunkInfo(final int chunkX, final int chunkZ, final WorldServer 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 WorldServer 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 String getChunkWaitInfo() { ++ synchronized (WAITING_CHUNKS) { ++ return WAITING_CHUNKS.toString(); ++ } ++ } ++ ++ public static void dumpAllChunkLoadInfo() { ++ synchronized (WAITING_CHUNKS) { ++ if (WAITING_CHUNKS.isEmpty()) { ++ return; ++ } ++ ++ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk wait task info below: "); ++ ++ for (final ChunkInfo chunkInfo : WAITING_CHUNKS) { ++ final ChunkLoadTask loadTask = chunkInfo.world.asyncChunkTaskManager.chunkLoadTasks.get(IOUtil.getCoordinateKey(chunkInfo.chunkX, chunkInfo.chunkZ)); ++ final ChunkSaveTask saveTask = chunkInfo.world.asyncChunkTaskManager.chunkSaveTasks.get(IOUtil.getCoordinateKey(chunkInfo.chunkX, chunkInfo.chunkZ)); ++ ++ PaperFileIOThread.LOGGER.log(Level.ERROR, chunkInfo.chunkX + "," + chunkInfo.chunkZ + " in '" + chunkInfo.world.getWorld().getName() + ":"); ++ PaperFileIOThread.LOGGER.log(Level.ERROR, "Load Task - " + (loadTask == null ? "none" : loadTask.toString())); ++ PaperFileIOThread.LOGGER.log(Level.ERROR, "Save Task - " + (saveTask == null ? "none" : saveTask.toString())); ++ } ++ } ++ } ++ ++ public static void initGlobalLoadThreads(int threads) { ++ if (threads <= 0 || globalWorkers != null) { ++ return; ++ } ++ ++ globalWorkers = new QueueExecutorThread[threads]; ++ globalQueue = new PrioritizedTaskQueue<>(); ++ ++ for (int i = 0; i < threads; ++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.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable); ++ }); ++ ++ globalWorkers[i].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 net.minecraft.server.ChunkProviderServer#serverThreadQueue ++ */ ++ public ChunkTaskManager(final WorldServer 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.getWorldData().getName()); ++ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1); ++ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { ++ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable); ++ }); ++ ++ this.workers[i].start(); ++ } ++ } ++ ++ /** ++ * 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(); ++ } ++ } ++ ++ /** ++ * 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 net.minecraft.server.ChunkProviderServer#serverThreadQueue ++ */ ++ public ChunkTaskManager(final WorldServer world) { ++ this.world = world; ++ this.workers = globalWorkers; ++ this.queue = globalQueue; ++ this.perWorldQueue = false; ++ } ++ ++ /** ++ * 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 WorldServer 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 NBTTagCompound 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 net.minecraft.server.ChunkRegionLoader.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 WorldServer 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 ChunkRegionLoader#getAsyncSaveData(WorldServer, IChunkAccess)} ++ * @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 ChunkRegionLoader.AsyncSaveData asyncSaveData, ++ final IChunkAccess chunk) { ++ AsyncCatcher.catchOp("chunk save schedule"); ++ ++ final WorldServer 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 IChunkAccess 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()) { ++ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).executeAll(); ++ } else { ++ CompletableFuture wait = new CompletableFuture<>(); ++ MinecraftServer.getServer().scheduleOnMain(() -> { ++ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).executeAll(); ++ }); ++ wait.join(); ++ } ++ return; ++ } ++ ++ 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)); ++ ++ ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(chunkKey); ++ if (chunkSaveTask != null) { ++ chunkSaveTask.raisePriority(priority); ++ if (chunkSaveTask.isScheduled() && chunkSaveTask.getPriority() != PrioritizedTaskQueue.COMPLETING_PRIORITY) { ++ // only notify if we're in queue to be executed ++ this.internalScheduleNotify(); ++ } ++ } ++ ++ ChunkLoadTask chunkLoadTask = this.chunkLoadTasks.get(chunkKey); ++ if (chunkLoadTask != null) { ++ chunkLoadTask.raisePriority(priority); ++ if (chunkLoadTask.isScheduled() && chunkLoadTask.getPriority() != PrioritizedTaskQueue.COMPLETING_PRIORITY) { ++ // only notify if we're in queue to be executed ++ this.internalScheduleNotify(); ++ } ++ } ++ } ++ ++ protected void internalSchedule(final ChunkTask task) { ++ if (this.workers == null) { ++ // execute() will execute immediately if we're main ++ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).addTask(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(); ++ } ++ ++ protected void internalScheduleNotify() { ++ for (final QueueExecutorThread worker : this.workers) { ++ if (worker.notifyTasks()) { ++ // break here since we only want to wake up one worker for scheduling one task ++ break; ++ } ++ } ++ } ++ ++} +diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java +index 3894b04342..9138a256bd 100644 +--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java ++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java +@@ -124,11 +124,137 @@ public class ChunkProviderServer extends IChunkProvider { + return playerChunk.getAvailableChunkNow(); + + } ++ ++ private long asyncLoadSeqCounter; ++ ++ public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) { ++ if (Thread.currentThread() != this.serverThread) { ++ this.serverThreadQueue.execute(() -> { ++ this.getChunkAtAsynchronously(x, z, gen, onComplete); ++ }); ++ return; ++ } ++ ++ long k = ChunkCoordIntPair.pair(x, z); ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); ++ ++ IChunkAccess ichunkaccess; ++ ++ // try cache ++ for (int l = 0; l < 4; ++l) { ++ if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) { ++ ichunkaccess = this.cacheChunk[l]; ++ if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime ++ ++ // move to first in cache ++ ++ for (int i1 = 3; i1 > 0; --i1) { ++ this.cachePos[i1] = this.cachePos[i1 - 1]; ++ this.cacheStatus[i1] = this.cacheStatus[i1 - 1]; ++ this.cacheChunk[i1] = this.cacheChunk[i1 - 1]; ++ } ++ ++ this.cachePos[0] = k; ++ this.cacheStatus[0] = ChunkStatus.FULL; ++ this.cacheChunk[0] = ichunkaccess; ++ ++ onComplete.accept((Chunk)ichunkaccess); ++ ++ return; ++ } ++ } ++ } ++ ++ if (gen) { ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } ++ ++ IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions ++ if (current != null) { ++ if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) { ++ onComplete.accept(null); // the chunk is not gen'd ++ return; ++ } ++ // we know the chunk is at full status here (either in read-only mode or the real thing) ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } ++ ++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); ++ ++ if (status != null && status != ChunkStatus.FULL) { ++ // does not exist on disk ++ onComplete.accept(null); ++ return; ++ } ++ ++ if (status == ChunkStatus.FULL) { ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } ++ ++ // status is null here ++ ++ // here we don't know what status it is and we're not supposed to generate ++ // so we asynchronously load empty status ++ ++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> { ++ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { ++ // the chunk on disk was not a full status chunk ++ onComplete.accept(null); ++ return; ++ } ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required ++ }); ++ } ++ ++ private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) { ++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete); ++ } ++ ++ private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) { ++ CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true); ++ Long identifier = Long.valueOf(this.asyncLoadSeqCounter++); ++ int ticketLevel = MCUtil.getTicketLevelFor(status); ++ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); ++ ++ future.whenCompleteAsync((Either either, Throwable throwable) -> { ++ // either left -> success ++ // either right -> failure ++ ++ if (throwable != null) { ++ throw new RuntimeException(throwable); ++ } ++ ++ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); ++ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading ++ ++ Optional failure = either.right(); ++ ++ if (failure.isPresent()) { ++ // failure ++ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString()); ++ } ++ ++ onComplete.accept(either.left().get()); ++ ++ }, this.serverThreadQueue); ++ } ++ ++ public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { ++ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); ++ } ++ ++ public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { ++ this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); ++ } + // Paper end + + @Nullable + @Override + public IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag) { ++ final int x = i; final int z = j; // Paper - conflict on variable change + if (Thread.currentThread() != this.serverThread) { + return (IChunkAccess) CompletableFuture.supplyAsync(() -> { + return this.getChunkAt(i, j, chunkstatus, flag); +@@ -150,8 +276,13 @@ public class ChunkProviderServer extends IChunkProvider { + CompletableFuture> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag); + + if (!completablefuture.isDone()) { // Paper ++ // Paper start - async chunk io/loading ++ this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); ++ com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.world, x, z); ++ // Paper end + this.world.timings.chunkAwait.startTiming(); // Paper + this.serverThreadQueue.awaitTasks(completablefuture::isDone); ++ com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug + this.world.timings.chunkAwait.stopTiming(); // Paper + } // Paper + ichunkaccess = (IChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { +@@ -631,11 +762,12 @@ public class ChunkProviderServer extends IChunkProvider { + protected boolean executeNext() { + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + try { ++ boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue(); // Paper + if (ChunkProviderServer.this.tickDistanceManager()) { + return true; + } else { + ChunkProviderServer.this.lightEngine.queueUpdate(); +- return super.executeNext(); ++ return super.executeNext() || execChunkTask; // Paper + } + } finally { + playerChunkMap.callbackExecutor.run(); +diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +index a028074112..98cc4efcf5 100644 +--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java ++++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +@@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + import it.unimi.dsi.fastutil.longs.LongSet; + import it.unimi.dsi.fastutil.shorts.ShortList; + import it.unimi.dsi.fastutil.shorts.ShortListIterator; ++import java.util.ArrayDeque; // Paper + import java.util.Arrays; + import java.util.BitSet; + import java.util.EnumSet; +@@ -22,7 +23,29 @@ public class ChunkRegionLoader { + + private static final Logger LOGGER = LogManager.getLogger(); + ++ // Paper start ++ public static final class InProgressChunkHolder { ++ ++ public final ProtoChunk protoChunk; ++ public final ArrayDeque tasks; ++ ++ public NBTTagCompound poiData; ++ ++ public InProgressChunkHolder(final ProtoChunk protoChunk, final ArrayDeque tasks) { ++ this.protoChunk = protoChunk; ++ this.tasks = tasks; ++ } ++ } ++ + public static ProtoChunk loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) { ++ InProgressChunkHolder holder = loadChunk(worldserver, definedstructuremanager, villageplace, chunkcoordintpair, nbttagcompound, true); ++ holder.tasks.forEach(Runnable::run); ++ return holder.protoChunk; ++ } ++ ++ public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) { ++ ArrayDeque tasksToExecuteOnMain = new ArrayDeque<>(); ++ // Paper end + ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator(); + WorldChunkManager worldchunkmanager = chunkgenerator.getWorldChunkManager(); + NBTTagCompound nbttagcompound1 = nbttagcompound.getCompound("Level"); +@@ -66,7 +89,9 @@ public class ChunkRegionLoader { + LightEngine lightengine = chunkproviderserver.getLightEngine(); + + if (flag) { +- lightengine.b(chunkcoordintpair, true); ++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main ++ lightengine.b(chunkcoordintpair, true); ++ }); // Paper - delay this task since we're executing off-main + } + + for (int k = 0; k < nbttaglist.size(); ++k) { +@@ -82,16 +107,30 @@ public class ChunkRegionLoader { + achunksection[b0] = chunksection; + } + +- villageplace.a(chunkcoordintpair, chunksection); ++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main ++ villageplace.a(chunkcoordintpair, chunksection); ++ }); // Paper - delay this task since we're executing off-main + } + + if (flag) { + if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) { +- lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("BlockLight"))); ++ // Paper start - delay this task since we're executing off-main ++ NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight")); ++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified ++ tasksToExecuteOnMain.add(() -> { ++ lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), blockLight); ++ }); ++ // Paper end + } + + if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) { +- lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("SkyLight"))); ++ // Paper start - delay this task since we're executing off-main ++ NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight")); ++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified ++ tasksToExecuteOnMain.add(() -> { ++ lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight); ++ }); ++ // Paper end + } + } + } +@@ -194,7 +233,7 @@ public class ChunkRegionLoader { + } + + if (chunkstatus_type == ChunkStatus.Type.LEVELCHUNK) { +- return new ProtoChunkExtension((Chunk) object); ++ return new InProgressChunkHolder(new ProtoChunkExtension((Chunk) object), tasksToExecuteOnMain); // Paper - Async chunk loading + } else { + ProtoChunk protochunk1 = (ProtoChunk) object; + +@@ -233,11 +272,83 @@ public class ChunkRegionLoader { + protochunk1.a(worldgenstage_features, BitSet.valueOf(nbttagcompound5.getByteArray(s1))); + } + +- return protochunk1; ++ return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading + } + } + ++ // Paper start - async chunk save for unload ++ public static final class AsyncSaveData { ++ public final NibbleArray[] blockLight; // null or size of 17 (for indices -1 through 15) ++ public final NibbleArray[] skyLight; ++ ++ public final NBTTagList blockTickList; // non-null if we had to go to the server's tick list ++ public final NBTTagList fluidTickList; // non-null if we had to go to the server's tick list ++ ++ public final long worldTime; ++ ++ public AsyncSaveData(NibbleArray[] blockLight, NibbleArray[] skyLight, NBTTagList blockTickList, NBTTagList fluidTickList, ++ long worldTime) { ++ this.blockLight = blockLight; ++ this.skyLight = skyLight; ++ this.blockTickList = blockTickList; ++ this.fluidTickList = fluidTickList; ++ this.worldTime = worldTime; ++ } ++ } ++ ++ // must be called sync ++ public static AsyncSaveData getAsyncSaveData(WorldServer world, IChunkAccess chunk) { ++ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save"); ++ ChunkCoordIntPair chunkPos = chunk.getPos(); ++ ++ LightEngineThreaded lightenginethreaded = world.getChunkProvider().getLightEngine(); ++ ++ NibbleArray[] blockLight = new NibbleArray[17 - (-1)]; ++ NibbleArray[] skyLight = new NibbleArray[17 - (-1)]; ++ ++ for (int i = -1; i < 17; ++i) { ++ NibbleArray blockArray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i)); ++ NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); ++ ++ // copy data for safety ++ if (blockArray != null) { ++ blockArray = blockArray.copy(); ++ } ++ if (skyArray != null) { ++ skyArray = skyArray.copy(); ++ } ++ ++ // apply offset of 1 for -1 starting index ++ blockLight[i + 1] = blockArray; ++ skyLight[i + 1] = skyArray; ++ } ++ ++ TickList blockTickList = chunk.n(); ++ ++ NBTTagList blockTickListSerialized; ++ if (blockTickList instanceof ProtoChunkTickList || blockTickList instanceof TickListChunk) { ++ blockTickListSerialized = null; ++ } else { ++ blockTickListSerialized = world.getBlockTickList().a(chunkPos); ++ } ++ ++ TickList fluidTickList = chunk.o(); ++ ++ NBTTagList fluidTickListSerialized; ++ if (fluidTickList instanceof ProtoChunkTickList || fluidTickList instanceof TickListChunk) { ++ fluidTickListSerialized = null; ++ } else { ++ fluidTickListSerialized = world.getFluidTickList().a(chunkPos); ++ } ++ ++ return new AsyncSaveData(blockLight, skyLight, blockTickListSerialized, fluidTickListSerialized, world.getTime()); ++ } ++ + public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess) { ++ return saveChunk(worldserver, ichunkaccess, null); ++ } ++ public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess, AsyncSaveData asyncsavedata) { ++ // Paper end + ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos(); + NBTTagCompound nbttagcompound = new NBTTagCompound(); + NBTTagCompound nbttagcompound1 = new NBTTagCompound(); +@@ -246,7 +357,7 @@ public class ChunkRegionLoader { + nbttagcompound.set("Level", nbttagcompound1); + nbttagcompound1.setInt("xPos", chunkcoordintpair.x); + nbttagcompound1.setInt("zPos", chunkcoordintpair.z); +- nbttagcompound1.setLong("LastUpdate", worldserver.getTime()); ++ nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading + nbttagcompound1.setLong("InhabitedTime", ichunkaccess.q()); + nbttagcompound1.setString("Status", ichunkaccess.getChunkStatus().d()); + ChunkConverter chunkconverter = ichunkaccess.p(); +@@ -262,14 +373,22 @@ public class ChunkRegionLoader { + + NBTTagCompound nbttagcompound2; + +- for (int i = -1; i < 17; ++i) { ++ for (int i = -1; i < 17; ++i) { // Paper - conflict on loop parameter change + int finalI = i; + ChunkSection chunksection = (ChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> { + return chunksection1 != null && chunksection1.getYPosition() >> 4 == finalI; + }).findFirst().orElse(Chunk.a); +- NibbleArray nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); +- NibbleArray nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); +- ++ // Paper start - async chunk save for unload ++ NibbleArray nibblearray; // block light ++ NibbleArray nibblearray1; // sky light ++ if (asyncsavedata == null) { ++ nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) ++ nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) ++ } else { ++ nibblearray = asyncsavedata.blockLight[i + 1]; // +1 to offset the -1 starting index ++ nibblearray1 = asyncsavedata.skyLight[i + 1]; // +1 to offset the -1 starting index ++ } ++ // Paper end + if (chunksection != Chunk.a || nibblearray != null || nibblearray1 != null) { + nbttagcompound2 = new NBTTagCompound(); + nbttagcompound2.setByte("Y", (byte) (i & 255)); +@@ -334,10 +453,10 @@ public class ChunkRegionLoader { + // Paper start + if ((int)Math.floor(entity.locX) >> 4 != chunk.getPos().x || (int)Math.floor(entity.locZ) >> 4 != chunk.getPos().z) { + LogManager.getLogger().warn(entity + " is not in this chunk, skipping save. This a bug fix to a vanilla bug. Do not report this to PaperMC please."); +- toUpdate.add(entity); ++ if (asyncsavedata == null) toUpdate.add(entity); // todo fix this broken code, entityJoinedWorld wont work in this case! + continue; + } +- if (entity.dead) { ++ if (asyncsavedata == null && entity.dead) { // todo + continue; + } + // Paper end +@@ -373,24 +492,32 @@ public class ChunkRegionLoader { + } + + nbttagcompound1.set("Entities", nbttaglist2); +- TickList ticklist = ichunkaccess.n(); ++ TickList ticklist = ichunkaccess.n(); // Paper - diff on method change (see getAsyncSaveData) + + if (ticklist instanceof ProtoChunkTickList) { + nbttagcompound1.set("ToBeTicked", ((ProtoChunkTickList) ticklist).b()); + } else if (ticklist instanceof TickListChunk) { +- nbttagcompound1.set("TileTicks", ((TickListChunk) ticklist).a(worldserver.getTime())); ++ nbttagcompound1.set("TileTicks", ((TickListChunk) ticklist).a(asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime())); // Paper - async chunk unloading ++ // Paper start - async chunk save for unload ++ } else if (asyncsavedata != null) { ++ nbttagcompound1.set("TileTicks", asyncsavedata.blockTickList); ++ // Paper end + } else { +- nbttagcompound1.set("TileTicks", worldserver.getBlockTickList().a(chunkcoordintpair)); ++ nbttagcompound1.set("TileTicks", worldserver.getBlockTickList().a(chunkcoordintpair)); // Paper - diff on method change (see getAsyncSaveData) + } + +- TickList ticklist1 = ichunkaccess.o(); ++ TickList ticklist1 = ichunkaccess.o(); // Paper - diff on method change (see getAsyncSaveData) + + if (ticklist1 instanceof ProtoChunkTickList) { + nbttagcompound1.set("LiquidsToBeTicked", ((ProtoChunkTickList) ticklist1).b()); + } else if (ticklist1 instanceof TickListChunk) { +- nbttagcompound1.set("LiquidTicks", ((TickListChunk) ticklist1).a(worldserver.getTime())); ++ nbttagcompound1.set("LiquidTicks", ((TickListChunk) ticklist1).a(asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime())); // Paper - async chunk unloading ++ // Paper start - async chunk save for unload ++ } else if (asyncsavedata != null) { ++ nbttagcompound1.set("LiquidTicks", asyncsavedata.fluidTickList); ++ // Paper end + } else { +- nbttagcompound1.set("LiquidTicks", worldserver.getFluidTickList().a(chunkcoordintpair)); ++ nbttagcompound1.set("LiquidTicks", worldserver.getFluidTickList().a(chunkcoordintpair)); // Paper - diff on method change (see getAsyncSaveData) + } + + nbttagcompound1.set("PostProcessing", a(ichunkaccess.l())); +diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java +index e324989b46..abb0d69d2f 100644 +--- a/src/main/java/net/minecraft/server/ChunkStatus.java ++++ b/src/main/java/net/minecraft/server/ChunkStatus.java +@@ -153,6 +153,7 @@ public class ChunkStatus { + return ChunkStatus.q.size(); + } + ++ public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER + public static int a(ChunkStatus chunkstatus) { + return ChunkStatus.r.getInt(chunkstatus.c()); + } +diff --git a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java +index d521d25cf5..84024e6ba4 100644 +--- a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java ++++ b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java +@@ -91,7 +91,7 @@ public abstract class IAsyncTaskHandler implements Mailbox public + while (this.executeNext()) { + ; + } +diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java +index 3f14392e6e..39f6ddb1d2 100644 +--- a/src/main/java/net/minecraft/server/IChunkLoader.java ++++ b/src/main/java/net/minecraft/server/IChunkLoader.java +@@ -3,6 +3,10 @@ package net.minecraft.server; + import com.mojang.datafixers.DataFixer; + import java.io.File; + import java.io.IOException; ++// Paper start ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++// Paper end + import java.util.function.Supplier; + import javax.annotation.Nullable; + +@@ -10,7 +14,9 @@ public class IChunkLoader extends RegionFileCache { + + protected final DataFixer b; + @Nullable +- private PersistentStructureLegacy a; ++ private volatile PersistentStructureLegacy a; // Paper - async chunk loading ++ ++ private final Object persistentDataLock = new Object(); // Paper + + public IChunkLoader(File file, DataFixer datafixer) { + super(file); +@@ -21,14 +27,18 @@ public class IChunkLoader extends RegionFileCache { + private boolean check(ChunkProviderServer cps, int x, int z) throws IOException { + ChunkCoordIntPair pos = new ChunkCoordIntPair(x, z); + if (cps != null) { +- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); +- if (cps.isLoaded(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; + } + } + + if (this.chunkExists(pos)) { +- NBTTagCompound nbt = read(pos); ++ // Paper start - prioritize ++ NBTTagCompound nbt = cps == null ? read(pos) : ++ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.loadChunkData((WorldServer)cps.getWorld(), x, z, ++ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHER_PRIORITY, false, true).chunkData; ++ // Paper end + if (nbt != null) { + NBTTagCompound level = nbt.getCompound("Level"); + if (level.getBoolean("TerrainPopulated")) { +@@ -65,11 +75,13 @@ public class IChunkLoader extends RegionFileCache { + if (i < 1493) { + nbttagcompound = GameProfileSerializer.a(this.b, DataFixTypes.CHUNK, nbttagcompound, i, 1493); + if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { ++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading + if (this.a == null) { + this.a = PersistentStructureLegacy.a(dimensionmanager.getType(), (WorldPersistentData) supplier.get()); // CraftBukkit - getType + } + + nbttagcompound = this.a.a(nbttagcompound); ++ } // Paper - Async chunk loading + } + } + +@@ -89,7 +101,9 @@ public class IChunkLoader extends RegionFileCache { + public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { + super.write(chunkcoordintpair, nbttagcompound); + if (this.a != null) { ++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading + this.a.a(chunkcoordintpair.pair()); ++ } // Paper - Async chunk loading + } + + } +diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java +index 23d1935dd5..14f8b61042 100644 +--- a/src/main/java/net/minecraft/server/MCUtil.java ++++ b/src/main/java/net/minecraft/server/MCUtil.java +@@ -530,4 +530,9 @@ public final class MCUtil { + out.print(fileData); + } + } ++ ++ public static int getTicketLevelFor(ChunkStatus status) { ++ // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean) ++ return 33 + ChunkStatus.getTicketLevelOffset(status); ++ } + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 2293360407..d2c0299730 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -774,6 +774,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant executor; + public final ChunkGenerator chunkGenerator; +- private final Supplier m; ++ private final Supplier m; public final Supplier getWorldPersistentDataSupplier() { return this.m; } // Paper - OBFHELPER + private final VillagePlace n; + public final LongSet unloadQueue; + private boolean updatingChunksModified; +@@ -72,7 +72,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + public final WorldLoadListener worldLoadListener; + public final PlayerChunkMap.a chunkDistanceManager; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.chunkDistanceManager; } // Paper - OBFHELPER + private final AtomicInteger v; +- private final DefinedStructureManager definedStructureManager; ++ public final DefinedStructureManager definedStructureManager; // Paper - private -> public + private final File x; + private final PlayerMap playerMap; + public final Int2ObjectMap trackedEntities; +@@ -133,7 +133,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + this.lightEngine = new LightEngineThreaded(ilightaccess, this, this.world.getWorldProvider().g(), threadedmailbox1, this.q.a(threadedmailbox1, false)); + this.chunkDistanceManager = new PlayerChunkMap.a(executor, iasynctaskhandler); + this.m = supplier; +- this.n = new VillagePlace(new File(this.x, "poi"), datafixer); ++ this.n = new VillagePlace(new File(this.x, "poi"), datafixer, this.world); // Paper + this.setViewDistance(i); + } + +@@ -293,6 +293,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + @Override + public void close() throws IOException { + this.q.close(); ++ this.world.asyncChunkTaskManager.close(true); // Paper - Required since we're closing regionfiles in the next line + this.n.close(); + super.close(); + } +@@ -313,7 +314,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + shouldSave = ((Chunk) ichunkaccess).lastSaved + world.paperConfig.autoSavePeriod <= world.getTime(); + } + +- if (shouldSave && this.saveChunk(ichunkaccess)) { ++ if (shouldSave && this.saveChunk(ichunkaccess, true)) { // Paper - async chunk io + ++savedThisTick; + playerchunk.m(); + } +@@ -364,6 +365,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + + }); ++ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour + } + + } +@@ -373,11 +375,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + protected void unloadChunks(BooleanSupplier booleansupplier) { + GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler(); + ++ try (Timing ignored = this.world.timings.poiUnload.startTiming()) { // Paper + gameprofilerfiller.enter("poi"); + this.n.a(booleansupplier); ++ } // Paper + gameprofilerfiller.exitEnter("chunk_unload"); + if (!this.world.isSavingDisabled()) { ++ try (Timing ignored = this.world.timings.chunkUnload.startTiming()) { // Paper + this.b(booleansupplier); ++ }// Paper + } + + gameprofilerfiller.exit(); +@@ -418,6 +424,60 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + } + ++ // 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#saveChunk(IChunkAccess, boolean), with the intentional difference being ++ // serializing the chunk is left to a worker thread. ++ private void asyncSave(IChunkAccess chunk) { ++ ChunkCoordIntPair chunkPos = chunk.getPos(); ++ NBTTagCompound poiData; ++ try (Timing ignored = this.world.timings.chunkUnloadPOISerialization.startTiming()) { ++ poiData = this.getVillagePlace().getData(chunk.getPos()); ++ } ++ ++ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z, ++ poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ ++ if (!chunk.isNeedsSaving()) { ++ return; ++ } ++ ++ ChunkStatus chunkstatus = chunk.getChunkStatus(); ++ ++ // Copied from PlayerChunkMap#saveChunk(IChunkAccess, boolean) ++ if (chunkstatus.getType() != ChunkStatus.Type.LEVELCHUNK) { ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveOverwriteCheck.startTiming()) { // Paper ++ // Paper start - Optimize save by using status cache ++ try { ++ ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkPos); ++ if (statusOnDisk != null && statusOnDisk.getType() == ChunkStatus.Type.LEVELCHUNK) { ++ // Paper end ++ return; ++ } ++ ++ if (chunkstatus == ChunkStatus.EMPTY && chunk.h().values().stream().noneMatch(StructureStart::e)) { ++ return; ++ } ++ } catch (IOException ex) { ++ ex.printStackTrace(); ++ return; ++ } ++ } ++ } ++ ++ ChunkRegionLoader.AsyncSaveData asyncSaveData; ++ try (Timing ignored = this.world.timings.chunkUnloadPrepareSave.startTiming()) { ++ asyncSaveData = ChunkRegionLoader.getAsyncSaveData(this.world, chunk); ++ } ++ ++ this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY, ++ asyncSaveData, chunk); ++ ++ chunk.setLastSaved(this.world.getTime()); ++ chunk.setNeedsSaving(false); ++ } ++ // Paper end ++ + private void a(long i, PlayerChunk playerchunk) { + CompletableFuture completablefuture = playerchunk.getChunkSave(); + Consumer consumer = (ichunkaccess) -> { // CraftBukkit - decompile error +@@ -431,13 +491,20 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + ((Chunk) ichunkaccess).setLoaded(false); + } + +- this.saveChunk(ichunkaccess); ++ //this.saveChunk(ichunkaccess);// Paper - delay + if (this.loadedChunks.remove(i) && ichunkaccess instanceof Chunk) { + Chunk chunk = (Chunk) ichunkaccess; + + this.world.unloadChunk(chunk); + } + ++ try { ++ this.asyncSave(ichunkaccess); // Paper - async chunk saving ++ } catch (Throwable ex) { ++ LOGGER.fatal("Failed to prepare async save, attempting synchronous save", ex); ++ this.saveChunk(ichunkaccess, true); ++ } ++ + this.lightEngine.a(ichunkaccess.getPos()); + this.lightEngine.queueUpdate(); + this.worldLoadListener.a(ichunkaccess.getPos(), (ChunkStatus) null); +@@ -507,26 +574,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + } + ++ // Paper start - Async chunk io ++ public NBTTagCompound completeChunkData(NBTTagCompound compound, ChunkCoordIntPair chunkcoordintpair) throws IOException { ++ return compound == null ? null : this.getChunkData(this.world.getWorldProvider().getDimensionManager(), this.getWorldPersistentDataSupplier(), compound, chunkcoordintpair, this.world); ++ } ++ // Paper end ++ + private CompletableFuture> f(ChunkCoordIntPair chunkcoordintpair) { +- return CompletableFuture.supplyAsync(() -> { ++ // Paper start - Async chunk io ++ final java.util.function.BiFunction> syncLoadComplete = (chunkHolder, ioThrowable) -> { + try (Timing ignored = this.world.timings.syncChunkLoadTimer.startTimingIfSync()) { // Paper +- NBTTagCompound nbttagcompound; // Paper +- try (Timing ignored2 = this.world.timings.chunkIOStage1.startTimingIfSync()) { // Paper +- nbttagcompound = this.readChunkData(chunkcoordintpair); ++ if (ioThrowable != null) { ++ com.destroystokyo.paper.io.IOUtil.rethrow(ioThrowable); + } +- +- if (nbttagcompound != null) { +- boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8); +- +- if (flag) { +- ProtoChunk protochunk = ChunkRegionLoader.loadChunk(this.world, this.definedStructureManager, this.n, chunkcoordintpair, nbttagcompound); +- +- protochunk.setLastSaved(this.world.getTime()); +- return Either.left(protochunk); +- } +- +- PlayerChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", chunkcoordintpair); ++ this.getVillagePlace().loadInData(chunkcoordintpair, chunkHolder.poiData); ++ chunkHolder.tasks.forEach(Runnable::run); ++ // Paper - async load completes this ++ // Paper end ++ ++ // Paper start - This is done async ++ if (chunkHolder.protoChunk != null) { ++ chunkHolder.protoChunk.setLastSaved(this.world.getTime()); ++ return Either.left(chunkHolder.protoChunk); + } ++ // Paper end + } catch (ReportedException reportedexception) { + Throwable throwable = reportedexception.getCause(); + +@@ -540,7 +611,27 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + + return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a, this.world)); // Paper - Anti-Xray +- }, this.executor); ++ // Paper start - Async chunk io ++ }; ++ CompletableFuture> ret = new CompletableFuture<>(); ++ ++ Consumer chunkHolderConsumer = (ChunkRegionLoader.InProgressChunkHolder holder) -> { ++ PlayerChunkMap.this.executor.addTask(() -> { ++ ret.complete(syncLoadComplete.apply(holder, null)); ++ }); ++ }; ++ ++ CompletableFuture chunkSaveFuture = this.world.asyncChunkTaskManager.getChunkSaveFuture(chunkcoordintpair.x, chunkcoordintpair.z); ++ if (chunkSaveFuture != null) { ++ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z, ++ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture); ++ this.world.asyncChunkTaskManager.raisePriority(chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY); ++ } else { ++ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z, ++ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false); ++ } ++ return ret; ++ // Paper end + } + + private CompletableFuture> b(PlayerChunk playerchunk, ChunkStatus chunkstatus) { +@@ -746,18 +837,43 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + return this.v.get(); + } + ++ // Paper start - async chunk io ++ private boolean writeDataAsync(ChunkCoordIntPair chunkPos, NBTTagCompound poiData, NBTTagCompound chunkData, boolean async) { ++ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z, ++ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ ++ if (async) { ++ return true; ++ } ++ ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSaveIOWait.startTiming()) { // Paper ++ Boolean successPoi = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, true); ++ Boolean successChunk = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, false); ++ ++ if (successPoi == Boolean.FALSE || successChunk == Boolean.FALSE) { ++ return false; ++ } ++ ++ // null indicates no task existed, which means our write completed before we waited on it ++ ++ return true; ++ } // Paper ++ } ++ // Paper end ++ + public boolean saveChunk(IChunkAccess ichunkaccess) { +- this.n.a(ichunkaccess.getPos()); ++ // Paper start - async param ++ return this.saveChunk(ichunkaccess, false); ++ } ++ public boolean saveChunk(IChunkAccess ichunkaccess, boolean async) { ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { ++ NBTTagCompound poiData = this.getVillagePlace().getData(ichunkaccess.getPos()); // Paper ++ //this.n.a(ichunkaccess.getPos()); // Delay ++ // Paper end + if (!ichunkaccess.isNeedsSaving()) { + return false; + } else { +- try { +- this.world.checkSession(); +- } catch (ExceptionWorldConflict exceptionworldconflict) { +- PlayerChunkMap.LOGGER.error("Couldn't save chunk; already in use by another instance of Minecraft?", exceptionworldconflict); +- com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exceptionworldconflict); // Paper +- return false; +- } ++ // Paper - The save session check is performed on the IO thread + + ichunkaccess.setLastSaved(this.world.getTime()); + ichunkaccess.setNeedsSaving(false); +@@ -768,27 +884,33 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + NBTTagCompound nbttagcompound; + + if (chunkstatus.getType() != ChunkStatus.Type.LEVELCHUNK) { ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveOverwriteCheck.startTiming()) { // Paper + // Paper start - Optimize save by using status cache + ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkcoordintpair); + if (statusOnDisk != null && statusOnDisk.getType() == ChunkStatus.Type.LEVELCHUNK) { + // Paper end ++ this.writeDataAsync(ichunkaccess.getPos(), poiData, null, async); // Paper - Async chunk io + return false; + } + + if (chunkstatus == ChunkStatus.EMPTY && ichunkaccess.h().values().stream().noneMatch(StructureStart::e)) { ++ this.writeDataAsync(ichunkaccess.getPos(), poiData, null, async); // Paper - Async chunk io + return false; + } + } +- ++ } // Paper ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveDataSerialization.startTiming()) { // Paper + nbttagcompound = ChunkRegionLoader.saveChunk(this.world, ichunkaccess); +- this.write(chunkcoordintpair, nbttagcompound); +- return true; ++ } // Paper ++ return this.writeDataAsync(ichunkaccess.getPos(), poiData, nbttagcompound, async); // Paper - Async chunk io ++ //return true; // Paper + } catch (Exception exception) { + PlayerChunkMap.LOGGER.error("Failed to save chunk {},{}", chunkcoordintpair.x, chunkcoordintpair.z, exception); + com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper + return false; + } + } ++ } // Paper + } + + protected void setViewDistance(int i) { +@@ -892,6 +1014,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + } + ++ // Paper start - Asynchronous chunk io ++ @Nullable ++ @Override ++ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException { ++ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) { ++ NBTTagCompound ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE ++ .loadChunkDataAsyncFuture(this.world, 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.read(chunkcoordintpair); ++ } ++ ++ @Override ++ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { ++ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) { ++ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave( ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound, ++ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); ++ ++ Boolean ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, ++ chunkcoordintpair.x, chunkcoordintpair.z, true, false); ++ ++ if (ret == Boolean.FALSE) { ++ throw new IOException("See logs for further detail"); ++ } ++ return; ++ } ++ super.write(chunkcoordintpair, nbttagcompound); ++ } ++ // Paper end ++ + @Nullable + public NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { // Paper - private -> public + NBTTagCompound nbttagcompound = this.read(chunkcoordintpair); +@@ -914,12 +1072,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + // Paper start - chunk status cache "api" + public ChunkStatus getChunkStatusOnDiskIfCached(ChunkCoordIntPair chunkPos) { ++ // Paper start - async chunk save for unload ++ IChunkAccess unloadingChunk = this.world.asyncChunkTaskManager.getChunkInSaveProgress(chunkPos.x, chunkPos.z); ++ if (unloadingChunk != null) { ++ return unloadingChunk.getChunkStatus(); ++ } ++ // Paper end ++ // Paper start - async io ++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false); ++ ++ if (inProgressWrite != null) { ++ return ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ + RegionFile regionFile = this.getRegionFileIfLoaded(chunkPos); + + return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + } + + public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException { ++ // Paper start - async chunk save for unload ++ IChunkAccess unloadingChunk = this.world.asyncChunkTaskManager.getChunkInSaveProgress(chunkPos.x, chunkPos.z); ++ if (unloadingChunk != null) { ++ return unloadingChunk.getChunkStatus(); ++ } ++ // Paper end ++ // Paper start - async io ++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false); ++ ++ if (inProgressWrite != null) { ++ return ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ synchronized (this) { // Paper - async io + RegionFile regionFile = this.getRegionFile(chunkPos, false); + + if (!regionFile.chunkExists(chunkPos)) { +@@ -931,18 +1119,56 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + if (status != null) { + return status; + } ++ // Paper start - async io ++ } + +- this.readChunkData(chunkPos); ++ NBTTagCompound compound = this.readChunkData(chunkPos); + +- return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); ++ return ChunkRegionLoader.getStatus(compound); ++ // Paper end + } + + public void updateChunkStatusOnDisk(ChunkCoordIntPair chunkPos, @Nullable NBTTagCompound compound) throws IOException { ++ synchronized (this) { // Paper - async io + RegionFile regionFile = this.getRegionFile(chunkPos, false); + + regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound)); ++ } // Paper - async io + } + ++ // Paper start - async io ++ // this function will not load chunk data off disk to check for status ++ // ret null for unknown, empty for empty status on disk or absent from disk ++ public ChunkStatus getStatusOnDiskNoLoad(int x, int z) { ++ // Paper start - async chunk save for unload ++ IChunkAccess unloadingChunk = this.world.asyncChunkTaskManager.getChunkInSaveProgress(x, z); ++ if (unloadingChunk != null) { ++ return unloadingChunk.getChunkStatus(); ++ } ++ // Paper end ++ // Paper start - async io ++ net.minecraft.server.NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, x, z, false); ++ ++ if (inProgressWrite != null) { ++ return net.minecraft.server.ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ // variant of PlayerChunkMap#getChunkStatusOnDisk that does not load data off disk, but loads the region file ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); ++ synchronized (world.getChunkProvider().playerChunkMap) { ++ net.minecraft.server.RegionFile file; ++ try { ++ file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); ++ } catch (IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ return !file.chunkExists(chunkPos) ? ChunkStatus.EMPTY : file.getStatusIfCached(x, z); ++ } ++ } ++ // Paper end ++ + public IChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { + PlayerChunk chunkHolder = this.pendingUnload.get(ChunkCoordIntPair.pair(chunkX, chunkZ)); + return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); +@@ -1290,6 +1516,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + } + ++ public VillagePlace getVillagePlace() { return this.h(); } // Paper - OBFHELPER + protected VillagePlace h() { + return this.n; + } +diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java +index a8c8ace46c..22144eb002 100644 +--- a/src/main/java/net/minecraft/server/RegionFile.java ++++ b/src/main/java/net/minecraft/server/RegionFile.java +@@ -343,7 +343,7 @@ public class RegionFile implements AutoCloseable { + this.d[j] = i; // Spigot - move this to after the write + } + +- public void close() throws IOException { ++ public synchronized void close() throws IOException { // Paper - synchronize + this.closed = true; // Paper + this.b.close(); + } +diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java +index d2b3289450..d3d6107422 100644 +--- a/src/main/java/net/minecraft/server/RegionFileCache.java ++++ b/src/main/java/net/minecraft/server/RegionFileCache.java +@@ -48,13 +48,13 @@ public abstract class RegionFileCache implements AutoCloseable { + } + + // Paper start +- public RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { ++ public synchronized RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { // Paper - synchronize for async io + return this.cache.getAndMoveToFirst(ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); + } + // Paper end + + public RegionFile getRegionFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { return this.a(chunkcoordintpair, existingOnly); } // Paper - OBFHELPER +- private RegionFile a(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit ++ private synchronized RegionFile a(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - synchronize for async io + long i = ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); + RegionFile regionfile = (RegionFile) this.cache.getAndMoveToFirst(i); + +@@ -338,7 +338,7 @@ public abstract class RegionFileCache implements AutoCloseable { + } + + // CraftBukkit start +- public boolean chunkExists(ChunkCoordIntPair pos) throws IOException { ++ public synchronized boolean chunkExists(ChunkCoordIntPair pos) throws IOException { // Paper - synchronize + copyIfNeeded(pos.x, pos.z); // Paper + RegionFile regionfile = a(pos, true); + +diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java +index 4b3e0c0f01..04b7dab646 100644 +--- a/src/main/java/net/minecraft/server/RegionFileSection.java ++++ b/src/main/java/net/minecraft/server/RegionFileSection.java +@@ -24,7 +24,7 @@ public class RegionFileSection extends RegionFi + + private static final Logger LOGGER = LogManager.getLogger(); + private final Long2ObjectMap> b = new Long2ObjectOpenHashMap(); +- private final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); ++ protected final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); // Paper - private -> protected + private final BiFunction, R> e; + private final Function f; + private final DataFixer g; +@@ -39,8 +39,8 @@ public class RegionFileSection extends RegionFi + } + + protected void a(BooleanSupplier booleansupplier) { +- while (!this.d.isEmpty() && booleansupplier.getAsBoolean()) { +- ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(this.d.firstLong()).u(); ++ while (!this.d.isEmpty() && booleansupplier.getAsBoolean()) { // Paper - conflict here to avoid obfhelpers ++ ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(this.d.firstLong()).u(); // Paper - conflict here to avoid obfhelpers + + this.d(chunkcoordintpair); + } +@@ -94,7 +94,12 @@ public class RegionFileSection extends RegionFi + } + + private void b(ChunkCoordIntPair chunkcoordintpair) { +- this.a(chunkcoordintpair, DynamicOpsNBT.a, this.c(chunkcoordintpair)); ++ // Paper start - load data in function ++ this.loadInData(chunkcoordintpair, this.c(chunkcoordintpair)); ++ } ++ public void loadInData(ChunkCoordIntPair chunkPos, NBTTagCompound compound) { ++ this.a(chunkPos, DynamicOpsNBT.a, compound); ++ // Paper end + } + + @Nullable +@@ -142,7 +147,7 @@ public class RegionFileSection extends RegionFi + } + + private void d(ChunkCoordIntPair chunkcoordintpair) { +- Dynamic dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a); ++ Dynamic dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a); // Paper - conflict here to avoid adding obfhelpers :) + NBTBase nbtbase = (NBTBase) dynamic.getValue(); + + if (nbtbase instanceof NBTTagCompound) { +@@ -157,6 +162,20 @@ public class RegionFileSection extends RegionFi + + } + ++ // Paper start - internal get data function, copied from above ++ private NBTTagCompound getDataInternal(ChunkCoordIntPair chunkcoordintpair) { ++ Dynamic dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a); ++ NBTBase nbtbase = (NBTBase) dynamic.getValue(); ++ ++ if (nbtbase instanceof NBTTagCompound) { ++ return (NBTTagCompound)nbtbase; ++ } else { ++ RegionFileSection.LOGGER.error("Expected compound tag, got {}", nbtbase); ++ } ++ return null; ++ } ++ // Paper end ++ + private Dynamic a(ChunkCoordIntPair chunkcoordintpair, DynamicOps dynamicops) { + Map map = Maps.newHashMap(); + +@@ -193,9 +212,9 @@ public class RegionFileSection extends RegionFi + public void a(ChunkCoordIntPair chunkcoordintpair) { + if (!this.d.isEmpty()) { + for (int i = 0; i < 16; ++i) { +- long j = SectionPosition.a(chunkcoordintpair, i).v(); ++ long j = SectionPosition.a(chunkcoordintpair, i).v(); // Paper - conflict here to avoid obfhelpers + +- if (this.d.contains(j)) { ++ if (this.d.contains(j)) { // Paper - conflict here to avoid obfhelpers + this.d(chunkcoordintpair); + return; + } +@@ -203,4 +222,21 @@ public class RegionFileSection extends RegionFi + } + + } ++ ++ // Paper start - get data function ++ public NBTTagCompound getData(ChunkCoordIntPair chunkcoordintpair) { ++ // Note: Copied from above ++ // This is checking if the data exists, then it builds it later in getDataInternal(ChunkCoordIntPair) ++ if (!this.d.isEmpty()) { ++ for (int i = 0; i < 16; ++i) { ++ long j = SectionPosition.a(chunkcoordintpair, i).v(); ++ ++ if (this.d.contains(j)) { ++ return this.getDataInternal(chunkcoordintpair); ++ } ++ } ++ } ++ return null; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java +index 9c114d2d37..e3150f85a5 100644 +--- a/src/main/java/net/minecraft/server/TicketType.java ++++ b/src/main/java/net/minecraft/server/TicketType.java +@@ -22,6 +22,7 @@ public class TicketType { + public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit + public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray ++ public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper + + public static TicketType a(String s, Comparator comparator) { + return new TicketType<>(s, comparator, 0L); +diff --git a/src/main/java/net/minecraft/server/VillagePlace.java b/src/main/java/net/minecraft/server/VillagePlace.java +index 3169590641..0e98b7803b 100644 +--- a/src/main/java/net/minecraft/server/VillagePlace.java ++++ b/src/main/java/net/minecraft/server/VillagePlace.java +@@ -20,8 +20,16 @@ public class VillagePlace extends RegionFileSection { + + private final VillagePlace.a a = new VillagePlace.a(); + ++ private final WorldServer world; // Paper ++ + public VillagePlace(File file, DataFixer datafixer) { ++ // Paper start ++ this(file, datafixer, null); ++ } ++ public VillagePlace(File file, DataFixer datafixer, WorldServer world) { ++ // Paper end + super(file, VillagePlaceSection::new, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK); ++ this.world = world; // Paper + } + + public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) { +@@ -121,7 +129,23 @@ public class VillagePlace extends RegionFileSection { + + @Override + public void a(BooleanSupplier booleansupplier) { +- super.a(booleansupplier); ++ // Paper start - async chunk io ++ if (this.world == null) { ++ super.a(booleansupplier); ++ } else { ++ //super.a(booleansupplier); // re-implement below ++ while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) { ++ ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).u(); ++ ++ NBTTagCompound 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.LOW_PRIORITY); ++ } ++ } ++ // Paper end + this.a.a(); + } + +@@ -207,6 +231,42 @@ public class VillagePlace extends RegionFileSection { + } + } + ++ // Paper start - Asynchronous chunk io ++ @javax.annotation.Nullable ++ @Override ++ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws java.io.IOException { ++ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) { ++ NBTTagCompound 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(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound 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()); ++ ++ Boolean ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, ++ chunkcoordintpair.x, chunkcoordintpair.z, true, true); ++ ++ if (ret == Boolean.FALSE) { ++ throw new java.io.IOException("See logs for further detail"); ++ } ++ return; ++ } ++ super.write(chunkcoordintpair, nbttagcompound); ++ } ++ // Paper end ++ + public static enum Occupancy { + + HAS_SPACE(VillagePlaceRecord::d), IS_OCCUPIED(VillagePlaceRecord::e), ANY((villageplacerecord) -> { +diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java +index 4497f6a601..cf981632c7 100644 +--- a/src/main/java/net/minecraft/server/WorldServer.java ++++ b/src/main/java/net/minecraft/server/WorldServer.java +@@ -78,6 +78,79 @@ public class WorldServer extends World { + return new Throwable(entity + " Added to world at " + new java.util.Date()); + } + ++ // 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, NBTTagCompound compound) throws java.io.IOException { ++ WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().write(new ChunkCoordIntPair(x, z), compound); ++ } ++ ++ @Override ++ public NBTTagCompound readData(int x, int z) throws java.io.IOException { ++ return WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().read(new ChunkCoordIntPair(x, z)); ++ } ++ ++ @Override ++ public T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function function) { ++ synchronized (WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace()) { ++ RegionFile file; ++ ++ try { ++ file = WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().getRegionFile(new ChunkCoordIntPair(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 (WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace()) { ++ RegionFile file = WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().getRegionFileIfLoaded(new ChunkCoordIntPair(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, NBTTagCompound compound) throws java.io.IOException { ++ WorldServer.this.getChunkProvider().playerChunkMap.write(new ChunkCoordIntPair(x, z), compound); ++ } ++ ++ @Override ++ public NBTTagCompound readData(int x, int z) throws java.io.IOException { ++ return WorldServer.this.getChunkProvider().playerChunkMap.read(new ChunkCoordIntPair(x, z)); ++ } ++ ++ @Override ++ public T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function function) { ++ synchronized (WorldServer.this.getChunkProvider().playerChunkMap) { ++ RegionFile file; ++ ++ try { ++ file = WorldServer.this.getChunkProvider().playerChunkMap.getRegionFile(new ChunkCoordIntPair(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 (WorldServer.this.getChunkProvider().playerChunkMap) { ++ RegionFile file = WorldServer.this.getChunkProvider().playerChunkMap.getRegionFileIfLoaded(new ChunkCoordIntPair(chunkX, chunkZ)); ++ return function.apply(file); ++ } ++ } ++ }; ++ public final com.destroystokyo.paper.io.chunk.ChunkTaskManager asyncChunkTaskManager; ++ // Paper end ++ + // Add env and gen to constructor + public WorldServer(MinecraftServer minecraftserver, Executor executor, WorldNBTStorage worldnbtstorage, WorldData worlddata, DimensionManager dimensionmanager, GameProfilerFiller gameprofilerfiller, WorldLoadListener worldloadlistener, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) { + super(worlddata, dimensionmanager, (world, worldprovider) -> { +@@ -121,6 +194,8 @@ public class WorldServer extends World { + + this.mobSpawnerTrader = this.worldProvider.getDimensionManager().getType() == DimensionManager.OVERWORLD ? new MobSpawnerTrader(this) : null; // CraftBukkit - getType() + this.getServer().addWorld(this.getWorld()); // CraftBukkit ++ ++ this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper + } + + public void doTick(BooleanSupplier booleansupplier) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 20e9fd8a79..0e98f00225 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -551,22 +551,23 @@ public class CraftWorld implements World { + return true; + } + +- net.minecraft.server.RegionFile file; +- try { +- file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); +- } catch (IOException ex) { +- throw new RuntimeException(ex); +- } ++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); // Paper - async io - move to own method + +- ChunkStatus status = file.getStatusIfCached(x, z); +- if (!file.chunkExists(chunkPos) || (status != null && status != ChunkStatus.FULL)) { ++ // Paper start - async io ++ if (status == ChunkStatus.EMPTY) { ++ // does not exist on disk + return false; + } + ++ if (status == null) { // at this stage we don't know what it is on disk + IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.EMPTY, true); + if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { + return false; + } ++ } else if (status != ChunkStatus.FULL) { ++ return false; // not full status on disk ++ } ++ // Paper end + + // fall through to load + // we do this so we do not re-read the chunk data on disk +@@ -2306,6 +2307,25 @@ public class CraftWorld implements World { + return (nearest == null) ? null : new Location(this, nearest.getX(), nearest.getY(), nearest.getZ()); + } + ++ // Paper start ++ @Override ++ public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) { ++ if (Bukkit.isPrimaryThread()) { ++ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z); ++ if (immediate != null) { ++ return CompletableFuture.completedFuture(immediate.bukkitChunk); ++ } ++ } ++ ++ CompletableFuture ret = new CompletableFuture<>(); ++ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> { ++ ret.complete(chunk == null ? null : chunk.bukkitChunk); ++ }); ++ ++ return ret; ++ } ++ // Paper end ++ + // Spigot start + @Override + public int getViewDistance() { +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index a1d93200e6..6ca0ebfdee 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -6,6 +6,7 @@ import java.lang.management.ThreadInfo; + import java.util.logging.Level; + import java.util.logging.Logger; + import com.destroystokyo.paper.PaperConfig; ++import com.destroystokyo.paper.io.chunk.ChunkTaskManager; // Paper + import net.minecraft.server.MinecraftServer; + import org.bukkit.Bukkit; + +@@ -83,6 +84,7 @@ public class WatchdogThread extends Thread + log.log( Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues" ); + log.log( Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports" ); + log.log( Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion() ); ++ ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper + // + if ( net.minecraft.server.World.lastPhysicsProblem != null ) + { +@@ -111,6 +113,7 @@ public class WatchdogThread extends Thread + // 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 ++ log.log( Level.SEVERE, "The server is waiting on these chunks: " + ChunkTaskManager.getChunkWaitInfo() ); // Paper - async chunk debug + dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // +-- +2.22.1 + diff --git a/Spigot-Server-Patches/0412-Reduce-sync-loads.patch b/Spigot-Server-Patches/0412-Reduce-sync-loads.patch new file mode 100644 index 0000000000..b34422f004 --- /dev/null +++ b/Spigot-Server-Patches/0412-Reduce-sync-loads.patch @@ -0,0 +1,315 @@ +From 2567891663da05271689ca3b429a3a678ceff42e Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Fri, 19 Jul 2019 03:29:14 -0700 +Subject: [PATCH] Reduce sync loads + +This reduces calls to getChunkAt which would load chunks. + +This patch also adds a tool to find calls which are doing this, however +it must be enabled by setting the startup flag -Dpaper.debug-sync-loads=true + +To get a debug log for sync loads, the command is /paper syncloadinfo + +diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java +index 09efbf7250..132397b3f3 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java ++++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java +@@ -1,9 +1,13 @@ + package com.destroystokyo.paper; + ++import com.destroystokyo.paper.io.SyncLoadFinder; + import com.google.common.base.Functions; + import com.google.common.collect.Iterables; + import com.google.common.collect.Lists; + import com.google.common.collect.Maps; ++import com.google.gson.JsonObject; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonWriter; + import net.minecraft.server.*; + import org.apache.commons.lang3.tuple.MutablePair; + import org.apache.commons.lang3.tuple.Pair; +@@ -18,6 +22,9 @@ import org.bukkit.craftbukkit.CraftWorld; + import org.bukkit.entity.Player; + + import java.io.File; ++import java.io.FileOutputStream; ++import java.io.PrintStream; ++import java.io.StringWriter; + import java.time.LocalDateTime; + import java.time.format.DateTimeFormatter; + import java.util.*; +@@ -130,6 +137,9 @@ public class PaperCommand extends Command { + case "chunkinfo": + doChunkInfo(sender, args); + break; ++ case "syncloadinfo": ++ this.doSyncLoadInfo(sender, args); ++ break; + case "ver": + case "version": + Command ver = org.bukkit.Bukkit.getServer().getCommandMap().getCommand("version"); +@@ -146,6 +156,40 @@ public class PaperCommand extends Command { + return true; + } + ++ private void doSyncLoadInfo(CommandSender sender, String[] args) { ++ if (!SyncLoadFinder.ENABLED) { ++ sender.sendMessage(ChatColor.RED + "This command requires the server startup flag '-Dpaper.debug-sync-loads=true' to be set."); ++ return; ++ } ++ File file = new File(new File(new File("."), "debug"), ++ "sync-load-info" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"); ++ file.getParentFile().mkdirs(); ++ sender.sendMessage(ChatColor.GREEN + "Writing sync load info to " + file.toString()); ++ ++ ++ try { ++ final JsonObject data = SyncLoadFinder.serialize(); ++ ++ 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, "UTF-8") ++ ) { ++ out.print(fileData); ++ } ++ sender.sendMessage(ChatColor.GREEN + "Successfully written sync load information!"); ++ } catch (Throwable thr) { ++ sender.sendMessage(ChatColor.RED + "Failed to write sync load information"); ++ thr.printStackTrace(); ++ } ++ } ++ + private void doChunkInfo(CommandSender sender, String[] args) { + List worlds; + if (args.length < 2 || args[1].equals("*")) { +diff --git a/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java +new file mode 100644 +index 0000000000..59aec10329 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java +@@ -0,0 +1,172 @@ ++package com.destroystokyo.paper.io; ++ ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.mojang.datafixers.util.Pair; ++import it.unimi.dsi.fastutil.longs.Long2IntMap; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import net.minecraft.server.World; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.WeakHashMap; ++ ++public class SyncLoadFinder { ++ ++ public static final boolean ENABLED = Boolean.getBoolean("paper.debug-sync-loads"); ++ ++ private static final WeakHashMap> SYNC_LOADS = new WeakHashMap<>(); ++ ++ private static final class SyncLoadInformation { ++ ++ public int times; ++ ++ public final Long2IntOpenHashMap coordinateTimes = new Long2IntOpenHashMap(); ++ } ++ ++ public static void logSyncLoad(final World world, final int chunkX, final int chunkZ) { ++ if (!ENABLED) { ++ return; ++ } ++ ++ final ThrowableWithEquals stacktrace = new ThrowableWithEquals(Thread.currentThread().getStackTrace()); ++ ++ SYNC_LOADS.compute(world, (final World keyInMap, Object2ObjectOpenHashMap map) -> { ++ if (map == null) { ++ map = new Object2ObjectOpenHashMap<>(); ++ } ++ ++ map.compute(stacktrace, (ThrowableWithEquals keyInMap0, SyncLoadInformation valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new SyncLoadInformation(); ++ } ++ ++ ++valueInMap.times; ++ ++ valueInMap.coordinateTimes.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (Long keyInMap1, Integer valueInMap1) -> { ++ return valueInMap1 == null ? Integer.valueOf(1) : Integer.valueOf(valueInMap1.intValue() + 1); ++ }); ++ ++ return valueInMap; ++ }); ++ ++ return map; ++ }); ++ } ++ ++ public static JsonObject serialize() { ++ final JsonObject ret = new JsonObject(); ++ ++ final JsonArray worldsData = new JsonArray(); ++ ++ for (final Map.Entry> entry : SYNC_LOADS.entrySet()) { ++ final World world = entry.getKey(); ++ ++ final JsonObject worldData = new JsonObject(); ++ ++ worldData.addProperty("name", world.getWorld().getName()); ++ ++ final List> data = new ArrayList<>(); ++ ++ entry.getValue().forEach((ThrowableWithEquals stacktrace, SyncLoadInformation times) -> { ++ data.add(new Pair<>(stacktrace, times)); ++ }); ++ ++ data.sort((Pair pair1, Pair pair2) -> { ++ return Integer.compare(pair2.getSecond().times, pair1.getSecond().times); // reverse order ++ }); ++ ++ final JsonArray stacktraces = new JsonArray(); ++ ++ for (Pair pair : data) { ++ final JsonObject stacktrace = new JsonObject(); ++ ++ stacktrace.addProperty("times", pair.getSecond().times); ++ ++ final JsonArray traces = new JsonArray(); ++ ++ for (StackTraceElement element : pair.getFirst().stacktrace) { ++ traces.add(String.valueOf(element)); ++ } ++ ++ stacktrace.add("stacktrace", traces); ++ ++ final JsonArray coordinates = new JsonArray(); ++ ++ for (Long2IntMap.Entry coordinate : pair.getSecond().coordinateTimes.long2IntEntrySet()) { ++ final long key = coordinate.getLongKey(); ++ final int times = coordinate.getIntValue(); ++ coordinates.add("(" + IOUtil.getCoordinateX(key) + "," + IOUtil.getCoordinateZ(key) + "): " + times); ++ } ++ ++ stacktrace.add("coordinates", coordinates); ++ ++ stacktraces.add(stacktrace); ++ } ++ ++ ++ worldData.add("stacktraces", stacktraces); ++ worldsData.add(worldData); ++ } ++ ++ ret.add("worlds", worldsData); ++ ++ return ret; ++ } ++ ++ static final class ThrowableWithEquals { ++ ++ private final StackTraceElement[] stacktrace; ++ private final int hash; ++ ++ public ThrowableWithEquals(final StackTraceElement[] stacktrace) { ++ this.stacktrace = stacktrace; ++ this.hash = ThrowableWithEquals.hash(stacktrace); ++ } ++ ++ public static int hash(final StackTraceElement[] stacktrace) { ++ int hash = 0; ++ ++ for (int i = 0; i < stacktrace.length; ++i) { ++ hash *= 31; ++ hash += stacktrace[i].hashCode(); ++ } ++ ++ return hash; ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.hash; ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (obj == null || obj.getClass() != this.getClass()) { ++ return false; ++ } ++ ++ final ThrowableWithEquals other = (ThrowableWithEquals)obj; ++ final StackTraceElement[] otherStackTrace = other.stacktrace; ++ ++ if (this.stacktrace.length != otherStackTrace.length) { ++ return false; ++ } ++ ++ if (this == obj) { ++ return true; ++ } ++ ++ for (int i = 0; i < this.stacktrace.length; ++i) { ++ if (!this.stacktrace[i].equals(otherStackTrace[i])) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java +index 9138a256bd..9f9bebdb22 100644 +--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java ++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java +@@ -280,6 +280,7 @@ public class ChunkProviderServer extends IChunkProvider { + this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); + com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.world, x, z); + // Paper end ++ com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.world, x, z); // Paper - sync load info + this.world.timings.chunkAwait.startTiming(); // Paper + this.serverThreadQueue.awaitTasks(completablefuture::isDone); + com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug +diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java +index b81b37445c..d3a0ed52bc 100644 +--- a/src/main/java/net/minecraft/server/World.java ++++ b/src/main/java/net/minecraft/server/World.java +@@ -1249,7 +1249,7 @@ public abstract class World implements IIBlockAccess, GeneratorAccess, AutoClose + + for (int i1 = i; i1 <= j; ++i1) { + for (int j1 = k; j1 <= l; ++j1) { +- Chunk chunk = this.getChunkProvider().getChunkAt(i1, j1, false); ++ Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper + + if (chunk != null) { + chunk.a(entity, axisalignedbb, list, predicate); +@@ -1269,7 +1269,7 @@ public abstract class World implements IIBlockAccess, GeneratorAccess, AutoClose + + for (int i1 = i; i1 < j; ++i1) { + for (int j1 = k; j1 < l; ++j1) { +- Chunk chunk = this.getChunkProvider().getChunkAt(i1, j1, false); ++ Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper + + if (chunk != null) { + chunk.a(entitytypes, axisalignedbb, list, predicate); +@@ -1291,7 +1291,7 @@ public abstract class World implements IIBlockAccess, GeneratorAccess, AutoClose + + for (int i1 = i; i1 < j; ++i1) { + for (int j1 = k; j1 < l; ++j1) { +- Chunk chunk = ichunkprovider.getChunkAt(i1, j1, false); ++ Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper + + if (chunk != null) { + chunk.a(oclass, axisalignedbb, list, predicate); +-- +2.22.1 +