From 8dbd6295e783f46c72245cfd14c87bcbeaf0ee22 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Thu, 22 Aug 2019 13:16:02 -0500
Subject: [PATCH] Updated Upstream (Paper)
Upstream has released updates that appears to apply and compile correctly
Paper Changes:
21faf822 Implement optional per player mobspawning (#2171)
88722ec8 Prevent consuming the wrong itemstack (#2477)
294e3046 Async chunk api/io/loading (#2308)
---
Paper | 2 +-
current-paper | 2 +-
patches/server/0001-Rebrand.patch | 20 +-
patches/server/0002-Purpur-config-files.patch | 12 +-
patches/server/0008-Silk-touch-spawners.patch | 10 +-
patches/server/0010-Integrate-ridables.patch | 10 +-
...e-giants-naturally-spawn-and-have-AI.patch | 8 +-
.../0020-Allow-color-codes-on-signs.patch | 14 +-
...030-Make-illusioners-naturally-spawn.patch | 8 +-
...ly-grow-cactus-reeds-bamboo-chorus-f.patch | 8 +-
...ance-for-zombie-horse-natural-spawns.patch | 8 +-
...042-Players-should-not-cram-to-death.patch | 8 +-
.../0046-Implement-lagging-threshold.patch | 8 +-
.../0048-Implement-elytra-settings.patch | 8 +-
...0050-Add-5-second-tps-average-in-tps.patch | 10 +-
...51-Asynchronous-chunk-IO-and-loading.patch | 3773 -----------------
...limit-packets-incoming-from-players.patch} | 12 +-
...-permissions-for-players-on-world-c.patch} | 2 +-
patches/server/0052-Reduce-sync-loads.patch | 322 --
...Add-blacklist-option-for-grindstone.patch} | 10 +-
...API.patch => 0054-Implement-AFK-API.patch} | 12 +-
...ement-optional-per-player-mob-spawns.patch | 287 --
...fs.patch => 0055-Advancement-stuffs.patch} | 2 +-
...-for-zombies-targetting-turtle-eggs.patch} | 2 +-
24 files changed, 88 insertions(+), 4470 deletions(-)
delete mode 100644 patches/server/0051-Asynchronous-chunk-IO-and-loading.patch
rename patches/server/{0053-Rate-limit-packets-incoming-from-players.patch => 0051-Rate-limit-packets-incoming-from-players.patch} (98%)
rename patches/server/{0055-Don-t-recalculate-permissions-for-players-on-world-c.patch => 0052-Don-t-recalculate-permissions-for-players-on-world-c.patch} (97%)
delete mode 100644 patches/server/0052-Reduce-sync-loads.patch
rename patches/server/{0056-Add-blacklist-option-for-grindstone.patch => 0053-Add-blacklist-option-for-grindstone.patch} (96%)
rename patches/server/{0057-Implement-AFK-API.patch => 0054-Implement-AFK-API.patch} (97%)
delete mode 100644 patches/server/0054-implement-optional-per-player-mob-spawns.patch
rename patches/server/{0058-Advancement-stuffs.patch => 0055-Advancement-stuffs.patch} (92%)
rename patches/server/{0059-Add-option-for-zombies-targetting-turtle-eggs.patch => 0056-Add-option-for-zombies-targetting-turtle-eggs.patch} (97%)
diff --git a/Paper b/Paper
index eee1a019f..21faf8227 160000
--- a/Paper
+++ b/Paper
@@ -1 +1 @@
-Subproject commit eee1a019f9ad018e1b700ce564dfadf6c5ad487c
+Subproject commit 21faf8227b57528cfa0825bb67b7e9674761c527
diff --git a/current-paper b/current-paper
index 8737119c1..cebbd8475 100644
--- a/current-paper
+++ b/current-paper
@@ -1 +1 @@
-1.14.4--b2e90788bff4a599b725842e53645df192494a41
+1.14.4--f74e7e96d920686142e0d0b0b28cc5661c2502a6
diff --git a/patches/server/0001-Rebrand.patch b/patches/server/0001-Rebrand.patch
index 6e53025dd..762625b71 100644
--- a/patches/server/0001-Rebrand.patch
+++ b/patches/server/0001-Rebrand.patch
@@ -1,4 +1,4 @@
-From 99abe96927a7347e1f3ab5604567ba0f66fd9b01 Mon Sep 17 00:00:00 2001
+From b67fd8714bdfb391a5b0c90e7d09e8526dd4e7b3 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sat, 4 May 2019 01:02:11 -0500
Subject: [PATCH] Rebrand
@@ -15,7 +15,7 @@ Subject: [PATCH] Rebrand
create mode 100644 src/main/java/net/pl3x/purpur/PurpurVersionFetcher.java
diff --git a/pom.xml b/pom.xml
-index beda5dc8a2..a12fa0163c 100644
+index beda5dc8a..a12fa0163 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,11 +1,11 @@
@@ -73,7 +73,7 @@ index beda5dc8a2..a12fa0163c 100644
diff --git a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
-index cd6e259239..bb227bc0fb 100644
+index cd6e25923..bb227bc0f 100644
--- a/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
+++ b/src/main/java/com/destroystokyo/paper/console/PaperConsole.java
@@ -17,7 +17,7 @@ public final class PaperConsole extends SimpleTerminalConsole {
@@ -86,10 +86,10 @@ index cd6e259239..bb227bc0fb 100644
);
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 01b389d89f..81ed2dfd25 100644
+index d2c029973..dcb783407 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1438,7 +1438,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant
Date: Thu, 9 May 2019 18:09:43 -0500
Subject: [PATCH] Purpur config files
@@ -20,10 +20,10 @@ Subject: [PATCH] Purpur config files
create mode 100644 src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-index 5942c3438..750ca9727 100644
+index 61eeb6747..58e2a0707 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-@@ -96,10 +96,12 @@ public class PaperConfig {
+@@ -97,10 +97,12 @@ public class PaperConfig {
MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Paper", entry.getValue());
}
@@ -57,7 +57,7 @@ index 5bc19cd08..c2870b054 100644
// Paper end
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
-index b81b37445..9f657b01f 100644
+index d3a0ed52b..eb6929e2b 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -97,6 +97,8 @@ public abstract class World implements IIBlockAccess, GeneratorAccess, AutoClose
@@ -990,7 +990,7 @@ index 18bce6fa3..4cd0bc64b 100644
ignoreVanillaPermissions = commandsConfiguration.getBoolean("ignore-vanilla-permissions");
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
-index 1fa81904d..2bc8773d4 100644
+index 460c447de..6c7ccfe18 100644
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -137,6 +137,14 @@ public class Main {
@@ -1009,5 +1009,5 @@ index 1fa81904d..2bc8773d4 100644
acceptsAll(asList("server-name"), "Name of the server")
.withRequiredArg()
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0008-Silk-touch-spawners.patch b/patches/server/0008-Silk-touch-spawners.patch
index 352f1e0ed..28631295b 100644
--- a/patches/server/0008-Silk-touch-spawners.patch
+++ b/patches/server/0008-Silk-touch-spawners.patch
@@ -1,4 +1,4 @@
-From c246b29621a8e2c62825e2a9d00a27f03e1c20b9 Mon Sep 17 00:00:00 2001
+From 8d144c45e81724e07c172eb3f7d06f6e4f105dfa Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Thu, 9 May 2019 14:27:37 -0500
Subject: [PATCH] Silk touch spawners
@@ -78,7 +78,7 @@ index bb77d916a..974a5d281 100644
return i;
diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java
-index a7fc34f85..e4fdf01dc 100644
+index 612b9b7e3..f8308c96b 100644
--- a/src/main/java/net/minecraft/server/EntityTypes.java
+++ b/src/main/java/net/minecraft/server/EntityTypes.java
@@ -135,10 +135,17 @@ public class EntityTypes {
@@ -99,7 +99,7 @@ index a7fc34f85..e4fdf01dc 100644
public static Optional> a(String s) {
return IRegistry.ENTITY_TYPE.getOptional(MinecraftKey.a(s));
}
-@@ -257,6 +264,12 @@ public class EntityTypes {
+@@ -258,6 +265,12 @@ public class EntityTypes {
return this.ba;
}
@@ -112,7 +112,7 @@ index a7fc34f85..e4fdf01dc 100644
public String f() {
if (this.bf == null) {
this.bf = SystemUtils.a("entity", IRegistry.ENTITY_TYPE.getKey(this));
-@@ -265,6 +278,7 @@ public class EntityTypes {
+@@ -266,6 +279,7 @@ public class EntityTypes {
return this.bf;
}
@@ -163,5 +163,5 @@ index 84646dbc2..987297634 100644
public static final Item bZ = a(Blocks.CHEST, CreativeModeTab.c);
public static final Item ca = a(Blocks.DIAMOND_ORE, CreativeModeTab.b);
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0010-Integrate-ridables.patch b/patches/server/0010-Integrate-ridables.patch
index 3713deb54..f5f7eee4f 100644
--- a/patches/server/0010-Integrate-ridables.patch
+++ b/patches/server/0010-Integrate-ridables.patch
@@ -1,4 +1,4 @@
-From 022df48e1fb9475f3a6ff5f2baf2ab7e4ae8947c Mon Sep 17 00:00:00 2001
+From 676acbfb7f8a5a1ca13643a7c5509e34309749b1 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Tue, 30 Apr 2019 19:17:21 -0500
Subject: [PATCH] Integrate ridables
@@ -1253,7 +1253,7 @@ index 8e463111b..2c34ab337 100644
this.goalSelector.a(2, new PathfinderGoalMoveTowardsTarget(this, 0.9D, 32.0F));
this.goalSelector.a(2, new PathfinderGoalStrollVillage(this, 0.6D));
diff --git a/src/main/java/net/minecraft/server/EntityLiving.java b/src/main/java/net/minecraft/server/EntityLiving.java
-index a661d109d..545283ed7 100644
+index 4b59f6081..d71fb40cb 100644
--- a/src/main/java/net/minecraft/server/EntityLiving.java
+++ b/src/main/java/net/minecraft/server/EntityLiving.java
@@ -87,10 +87,10 @@ public abstract class EntityLiving extends Entity {
@@ -2449,10 +2449,10 @@ index 0bd80e562..6bd2aeaad 100644
if (this.h == ControllerMove.Operation.MOVE_TO && !this.i.getNavigation().n()) {
double d0 = this.b - this.i.locX;
diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java
-index e4fdf01dc..84aa1db72 100644
+index f8308c96b..f8600cc07 100644
--- a/src/main/java/net/minecraft/server/EntityTypes.java
+++ b/src/main/java/net/minecraft/server/EntityTypes.java
-@@ -265,6 +265,10 @@ public class EntityTypes {
+@@ -266,6 +266,10 @@ public class EntityTypes {
}
// Purpur start
@@ -3215,5 +3215,5 @@ index 56c233872..b19970c88 100644
+ // Purpur end
}
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0011-Make-giants-naturally-spawn-and-have-AI.patch b/patches/server/0011-Make-giants-naturally-spawn-and-have-AI.patch
index e3c6f7e18..cb9af84a1 100644
--- a/patches/server/0011-Make-giants-naturally-spawn-and-have-AI.patch
+++ b/patches/server/0011-Make-giants-naturally-spawn-and-have-AI.patch
@@ -1,4 +1,4 @@
-From 16dc587c408f455255d4f5d1cbade8824e6cc24c Mon Sep 17 00:00:00 2001
+From 609835c609d98c4315db23388bca76fc157630c9 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sun, 12 May 2019 00:43:12 -0500
Subject: [PATCH] Make giants naturally spawn and have AI
@@ -103,10 +103,10 @@ index 29e7639ad..e69e7a456 100644
}
}
diff --git a/src/main/java/net/minecraft/server/SpawnerCreature.java b/src/main/java/net/minecraft/server/SpawnerCreature.java
-index 5e6559df0..2d4ea99f1 100644
+index 9d4a96ae4..49d1b7898 100644
--- a/src/main/java/net/minecraft/server/SpawnerCreature.java
+++ b/src/main/java/net/minecraft/server/SpawnerCreature.java
-@@ -78,6 +78,14 @@ public final class SpawnerCreature {
+@@ -77,6 +77,14 @@ public final class SpawnerCreature {
if (biomebase_biomemeta.b.e() != EnumCreatureType.MISC && (biomebase_biomemeta.b.d() || d0 <= 16384.0D)) {
EntityTypes> entitytypes = biomebase_biomemeta.b;
@@ -140,5 +140,5 @@ index 61adf7d63..7063c6329 100644
public static float crystalsAttackPhantomDamage = 1.0F;
public static double phantomsOrbitCrystalsRadius = 0.0D;
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0020-Allow-color-codes-on-signs.patch b/patches/server/0020-Allow-color-codes-on-signs.patch
index 286f93820..0ccce4f88 100644
--- a/patches/server/0020-Allow-color-codes-on-signs.patch
+++ b/patches/server/0020-Allow-color-codes-on-signs.patch
@@ -1,4 +1,4 @@
-From ff15aa3bed02c1d829c032ee6aa11b6b9f601d22 Mon Sep 17 00:00:00 2001
+From 5ea636f8b88a6d8259fb3a9a5c77e5deb3ede4bb Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Thu, 6 Jun 2019 17:40:30 -0500
Subject: [PATCH] Allow color codes on signs
@@ -11,10 +11,10 @@ Subject: [PATCH] Allow color codes on signs
4 files changed, 21 insertions(+)
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
-index 18695d9b..8c59692b 100644
+index 5c4a4be1b..af3c7e595 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
-@@ -1114,6 +1114,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+@@ -1120,6 +1120,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
@Override
public void openSign(TileEntitySign tileentitysign) {
tileentitysign.a((EntityHuman) this);
@@ -23,7 +23,7 @@ index 18695d9b..8c59692b 100644
}
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
-index e7b8b2e9..520490d5 100644
+index e7b8b2e99..520490d59 100644
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
@@ -2561,6 +2561,7 @@ public class PlayerConnection implements PacketListenerPlayIn {
@@ -35,7 +35,7 @@ index e7b8b2e9..520490d5 100644
}
SignChangeEvent event = new SignChangeEvent((org.bukkit.craftbukkit.block.CraftBlock) player.getWorld().getBlockAt(x, y, z), this.server.getPlayer(this.player), lines);
diff --git a/src/main/java/net/minecraft/server/TileEntitySign.java b/src/main/java/net/minecraft/server/TileEntitySign.java
-index 0a8d9b52..65771ed9 100644
+index 0a8d9b52d..65771ed9e 100644
--- a/src/main/java/net/minecraft/server/TileEntitySign.java
+++ b/src/main/java/net/minecraft/server/TileEntitySign.java
@@ -122,6 +122,20 @@ public class TileEntitySign extends TileEntity implements ICommandListener { //
@@ -60,7 +60,7 @@ index 0a8d9b52..65771ed9 100644
@Override
public PacketPlayOutTileEntityData getUpdatePacket() {
diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
-index b1d89450..82643156 100644
+index 383e7d5d5..f455c5f67 100644
--- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
@@ -98,4 +98,9 @@ public class PurpurWorldConfig {
@@ -74,5 +74,5 @@ index b1d89450..82643156 100644
+ }
}
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0030-Make-illusioners-naturally-spawn.patch b/patches/server/0030-Make-illusioners-naturally-spawn.patch
index 58c9a62c8..cac9cbdad 100644
--- a/patches/server/0030-Make-illusioners-naturally-spawn.patch
+++ b/patches/server/0030-Make-illusioners-naturally-spawn.patch
@@ -1,4 +1,4 @@
-From 309c1c6f884bf0fb66d5e7f6226fe0324314af41 Mon Sep 17 00:00:00 2001
+From 66cc6181d6b8b5a3c2b962ba058a83cc64d54494 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Fri, 5 Jul 2019 11:09:25 -0500
Subject: [PATCH] Make illusioners naturally spawn
@@ -39,10 +39,10 @@ index b075ee4ec..48dcab2d9 100644
private static void addSpawn(String biome, EnumCreatureType ct, EntityTypes et, int weight, int min_group, int max_group) {
diff --git a/src/main/java/net/minecraft/server/SpawnerCreature.java b/src/main/java/net/minecraft/server/SpawnerCreature.java
-index 2d4ea99f1..62fc61df2 100644
+index 49d1b7898..a60ada26d 100644
--- a/src/main/java/net/minecraft/server/SpawnerCreature.java
+++ b/src/main/java/net/minecraft/server/SpawnerCreature.java
-@@ -83,6 +83,10 @@ public final class SpawnerCreature {
+@@ -82,6 +82,10 @@ public final class SpawnerCreature {
if (!net.pl3x.purpur.PurpurConfig.giantsNaturallySpawn) {
return amountSpawned;
}
@@ -70,5 +70,5 @@ index 412a120e2..76556e7ea 100644
private static void ironGolemSettings() {
ironGolemSwims = getBoolean("settings.mobs.iron_golem.swims", ironGolemSwims);
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0035-Fix-MC-113809-Only-grow-cactus-reeds-bamboo-chorus-f.patch b/patches/server/0035-Fix-MC-113809-Only-grow-cactus-reeds-bamboo-chorus-f.patch
index f673045e8..37b6950b5 100644
--- a/patches/server/0035-Fix-MC-113809-Only-grow-cactus-reeds-bamboo-chorus-f.patch
+++ b/patches/server/0035-Fix-MC-113809-Only-grow-cactus-reeds-bamboo-chorus-f.patch
@@ -1,4 +1,4 @@
-From 58bcc5ac517eaea08870e09e86ebe44e4e38f32e Mon Sep 17 00:00:00 2001
+From 6671cfadc33e835c678b2b33c32b0c9cb80045c0 Mon Sep 17 00:00:00 2001
From: Phoenix616
Date: Thu, 16 May 2019 01:33:46 +0100
Subject: [PATCH] Fix MC-113809 Only grow cactus/reeds/bamboo/chorus fruit on
@@ -74,10 +74,10 @@ index ff674a9d5..4a13e248e 100644
for (i = 1; world.getType(blockposition.down(i)).getBlock() == this; ++i) {
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index 451ad4f32..a41d050cf 100644
+index 5eff278e7..163d4fa85 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -452,7 +452,9 @@ public class WorldServer extends World {
+@@ -527,7 +527,9 @@ public class WorldServer extends World {
IBlockData iblockdata = chunksection.getType(blockposition2.getX() - j, blockposition2.getY() - j1, blockposition2.getZ() - k);
if (iblockdata.q()) {
@@ -88,5 +88,5 @@ index 451ad4f32..a41d050cf 100644
Fluid fluid = iblockdata.p();
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0036-Add-chance-for-zombie-horse-natural-spawns.patch b/patches/server/0036-Add-chance-for-zombie-horse-natural-spawns.patch
index 1edf0968c..9a5614a03 100644
--- a/patches/server/0036-Add-chance-for-zombie-horse-natural-spawns.patch
+++ b/patches/server/0036-Add-chance-for-zombie-horse-natural-spawns.patch
@@ -1,4 +1,4 @@
-From dc4fde5a01f65b131eb2517455b3c0cd4c88c47c Mon Sep 17 00:00:00 2001
+From d8364e2ffae55ab7981db722fffa74d00fd27122 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sun, 7 Jul 2019 19:52:16 -0500
Subject: [PATCH] Add chance for zombie horse natural spawns
@@ -9,10 +9,10 @@ Subject: [PATCH] Add chance for zombie horse natural spawns
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index a41d050cf..e73257f3f 100644
+index 163d4fa85..f996c2c24 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -402,12 +402,18 @@ public class WorldServer extends World {
+@@ -477,12 +477,18 @@ public class WorldServer extends World {
boolean flag1 = this.getGameRules().getBoolean(GameRules.DO_MOB_SPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.b() * paperConfig.skeleHorseSpawnChance; // Paper
if (flag1) {
@@ -54,5 +54,5 @@ index e515f4775..c7e634ecc 100644
private static void largeEnderChests() {
largeEnderChests = getBoolean("settings.large-ender-chests", largeEnderChests);
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0042-Players-should-not-cram-to-death.patch b/patches/server/0042-Players-should-not-cram-to-death.patch
index 4b69d0538..f5e127c4d 100644
--- a/patches/server/0042-Players-should-not-cram-to-death.patch
+++ b/patches/server/0042-Players-should-not-cram-to-death.patch
@@ -1,4 +1,4 @@
-From 096f2f316f27ddf82c1575dc13fc1d80f3346541 Mon Sep 17 00:00:00 2001
+From 263a96137b30b81c1887e968d3e224fe0bf450a0 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sun, 21 Jul 2019 18:01:46 -0500
Subject: [PATCH] Players should not cram to death
@@ -8,10 +8,10 @@ Subject: [PATCH] Players should not cram to death
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
-index 8c59692b2..d375069f3 100644
+index af3c7e595..f9469e9e7 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
-@@ -1073,7 +1073,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+@@ -1079,7 +1079,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
@Override
public boolean isInvulnerable(DamageSource damagesource) {
@@ -21,5 +21,5 @@ index 8c59692b2..d375069f3 100644
@Override
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0046-Implement-lagging-threshold.patch b/patches/server/0046-Implement-lagging-threshold.patch
index 274297351..3b163ea06 100644
--- a/patches/server/0046-Implement-lagging-threshold.patch
+++ b/patches/server/0046-Implement-lagging-threshold.patch
@@ -1,4 +1,4 @@
-From 875c115e2ca08999d98deb68770ab057795ebc5f Mon Sep 17 00:00:00 2001
+From a2080753291fba978ee9dfaeb08d7d5d7f661ebd Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Tue, 23 Jul 2019 10:07:16 -0500
Subject: [PATCH] Implement lagging threshold
@@ -10,7 +10,7 @@ Subject: [PATCH] Implement lagging threshold
3 files changed, 12 insertions(+)
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index e9eb126a5..95b996443 100644
+index dcb783407..427c1da00 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -175,6 +175,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant
Date: Thu, 25 Jul 2019 18:07:37 -0500
Subject: [PATCH] Implement elytra settings
@@ -13,7 +13,7 @@ Subject: [PATCH] Implement elytra settings
6 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/src/main/java/net/minecraft/server/EntityLiving.java b/src/main/java/net/minecraft/server/EntityLiving.java
-index bc701552e..757b47b80 100644
+index b4230fcc4..213683674 100644
--- a/src/main/java/net/minecraft/server/EntityLiving.java
+++ b/src/main/java/net/minecraft/server/EntityLiving.java
@@ -2592,9 +2592,16 @@ public abstract class EntityLiving extends Entity {
@@ -36,7 +36,7 @@ index bc701552e..757b47b80 100644
}
} else {
flag = false;
-@@ -3236,6 +3243,7 @@ public abstract class EntityLiving extends Entity {
+@@ -3240,6 +3247,7 @@ public abstract class EntityLiving extends Entity {
}
}
@@ -145,5 +145,5 @@ index 7ff20fe08..b1dcb5f8d 100644
+ }
}
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0050-Add-5-second-tps-average-in-tps.patch b/patches/server/0050-Add-5-second-tps-average-in-tps.patch
index 3cff56b84..5cea51bf0 100644
--- a/patches/server/0050-Add-5-second-tps-average-in-tps.patch
+++ b/patches/server/0050-Add-5-second-tps-average-in-tps.patch
@@ -1,4 +1,4 @@
-From fcca21ceed90f2d76e6fa826431205db2351d56b Mon Sep 17 00:00:00 2001
+From e019732e0457b7a838f9c1934c9a1efb6529c8e7 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sun, 28 Jul 2019 01:27:37 -0500
Subject: [PATCH] Add 5 second tps average in /tps
@@ -10,7 +10,7 @@ Subject: [PATCH] Add 5 second tps average in /tps
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 95b996443..443e0d627 100644
+index 427c1da00..55aba2157 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -174,7 +174,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant
-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).
----
- .../co/aikar/timings/WorldTimingsHandler.java | 22 +
- .../com/destroystokyo/paper/PaperConfig.java | 61 ++
- .../ChunkPacketBlockControllerAntiXray.java | 46 +-
- .../paper/io/ConcreteFileIOThread.java | 665 ++++++++++++++++++
- .../com/destroystokyo/paper/io/IOUtil.java | 62 ++
- .../paper/io/PrioritizedTaskQueue.java | 258 +++++++
- .../paper/io/QueueExecutorThread.java | 244 +++++++
- .../paper/io/chunk/ChunkLoadTask.java | 149 ++++
- .../paper/io/chunk/ChunkSaveTask.java | 114 +++
- .../paper/io/chunk/ChunkTask.java | 40 ++
- .../paper/io/chunk/ChunkTaskManager.java | 336 +++++++++
- .../minecraft/server/ChunkProviderServer.java | 129 ++++
- .../minecraft/server/ChunkRegionLoader.java | 157 ++++-
- .../net/minecraft/server/ChunkStatus.java | 1 +
- .../minecraft/server/IAsyncTaskHandler.java | 2 +-
- .../net/minecraft/server/IChunkLoader.java | 37 +-
- .../java/net/minecraft/server/MCUtil.java | 5 +
- .../net/minecraft/server/MinecraftServer.java | 1 +
- .../net/minecraft/server/NibbleArray.java | 1 +
- .../net/minecraft/server/PlayerChunk.java | 8 +-
- .../net/minecraft/server/PlayerChunkMap.java | 296 +++++++-
- .../java/net/minecraft/server/RegionFile.java | 2 +-
- .../net/minecraft/server/RegionFileCache.java | 6 +-
- .../minecraft/server/RegionFileSection.java | 56 +-
- .../java/net/minecraft/server/TicketType.java | 1 +
- .../net/minecraft/server/VillagePlace.java | 66 +-
- .../net/minecraft/server/WorldServer.java | 77 +-
- .../org/bukkit/craftbukkit/CraftWorld.java | 36 +-
- 28 files changed, 2787 insertions(+), 91 deletions(-)
- create mode 100644 src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/IOUtil.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
- create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
-
-diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-index 92c32c48d..f4d5db02f 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 750ca9727..58e2a0707 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;
-
-@@ -390,4 +391,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 23626bef3..1edcecd2e 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/ConcreteFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
-new file mode 100644
-index 000000000..7fb74810e
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java
-@@ -0,0 +1,665 @@
-+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 ConcreteFileIOThread 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 ConcreteFileIOThread INSTANCE = new ConcreteFileIOThread();
-+
-+ static {
-+ INSTANCE.start();
-+ }
-+ }
-+
-+ private final AtomicLong writeCounter = new AtomicLong();
-+
-+ private ConcreteFileIOThread() {
-+ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time
-+ this.setName("Concrete RegionFile IO Thread");
-+ this.setPriority(Thread.NORM_PRIORITY - 1); // we keep priority close to normal because threads can wait on us
-+ this.setUncaughtExceptionHandler((final Thread unused, final Throwable thr) -> {
-+ 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() != null);
-+ }
-+
-+ // wait for write
-+ ChunkDataController.InProgressWrite write = task.inProgressWrite;
-+ if (write == null) {
-+ return null;
-+ }
-+ return Boolean.valueOf(write.wrote.join() != null);
-+ }
-+ // 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;
-+
-+ ConcreteFileIOThread.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);
-+
-+ ConcreteFileIOThread.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);
-+ ConcreteFileIOThread.Holder.INSTANCE.queueTask(this);
-+ }
-+
-+ @Override
-+ public void run() {
-+ ChunkDataController.InProgressRead read = this.inProgressRead;
-+ if (read != null) {
-+ NBTTagCompound compound = ConcreteFileIOThread.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(ConcreteFileIOThread.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;
-+ boolean[] returnFailWrite = new boolean[] { false };
-+
-+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ ChunkDataTask.this.inProgressWrite.wrote.complete(ConcreteFileIOThread.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(ConcreteFileIOThread.FAILURE_VALUE); // Hack
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+ if (finalFailWrite) {
-+ if (valueInMap.inProgressWrite.writeCounter == writeCounter) {
-+ valueInMap.inProgressWrite.writeCounter = -1L;
-+ returnFailWrite[0] = true;
-+ }
-+ // Hack start
-+ valueInMap.inProgressWrite.wrote.complete(ConcreteFileIOThread.FAILURE_VALUE);
-+ return valueInMap;
-+ }
-+ if (valueInMap.inProgressWrite.writeCounter == writeCounter) {
-+ valueInMap.inProgressWrite.wrote.complete(data);
-+ return null;
-+ }
-+ return valueInMap;
-+ // Hack end
-+ });
-+
-+ if (inMap == null || returnFailWrite[0]) {
-+ // 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/IOUtil.java b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
-new file mode 100644
-index 000000000..5af0ac3d9
---- /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/PrioritizedTaskQueue.java b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
-new file mode 100644
-index 000000000..c3ca3c4a1
---- /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 000000000..f127ef236
---- /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 000000000..193ea0c6f
---- /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.ConcreteFileIOThread;
-+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 ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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 ConcreteFileIOThread.ChunkData chunkData = this.chunkData;
-+
-+ if (chunkData.poiData == ConcreteFileIOThread.FAILURE_VALUE || chunkData.chunkData == ConcreteFileIOThread.FAILURE_VALUE) {
-+ ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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 000000000..d1c0d450e
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
-@@ -0,0 +1,114 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import co.aikar.timings.Timing;
-+import com.destroystokyo.paper.io.ConcreteFileIOThread;
-+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.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+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?
-+ ConcreteFileIOThread.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
-+ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).addTask(() -> {
-+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTiming()) {
-+ NBTTagCompound data = ConcreteFileIOThread.FAILURE_VALUE;
-+
-+ try {
-+ data = ChunkRegionLoader.saveChunk(this.world, this.chunk, this.asyncSaveData);
-+ ConcreteFileIOThread.LOGGER.info("Successfully serialized chunk data for task: " + this.toString() + " synchronously");
-+ } catch (final Throwable ex1) {
-+ ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
-+ }
-+ if (compound != ConcreteFileIOThread.FAILURE_VALUE) {
-+ ConcreteFileIOThread.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 000000000..400fae5d0
---- /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.ConcreteFileIOThread;
-+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();
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final int priority) {
-+ ConcreteFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority);
-+ return super.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public boolean updatePriority(final int priority) {
-+ ConcreteFileIOThread.Holder.INSTANCE.setPriority(this.world, this.chunkX, this.chunkZ, priority);
-+ return super.updatePriority(priority);
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
-new file mode 100644
-index 000000000..03cb8e0b3
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
-@@ -0,0 +1,336 @@
-+package com.destroystokyo.paper.io.chunk;
-+
-+import com.destroystokyo.paper.io.ConcreteFileIOThread;
-+import com.destroystokyo.paper.io.IOUtil;
-+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
-+import com.destroystokyo.paper.io.QueueExecutorThread;
-+import net.minecraft.server.ChunkRegionLoader;
-+import net.minecraft.server.IAsyncTaskHandler;
-+import net.minecraft.server.IChunkAccess;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.NBTTagCompound;
-+import net.minecraft.server.WorldServer;
-+import org.bukkit.Bukkit;
-+import org.spigotmc.AsyncCatcher;
-+
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.ConcurrentHashMap;
-+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;
-+
-+ 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("Async chunk loader thread #" + i);
-+ globalWorkers[i].setPriority(Thread.NORM_PRIORITY - 1);
-+ globalWorkers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
-+ ConcreteFileIOThread.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) -> {
-+ ConcreteFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
-+ });
-+
-+ this.workers[i].start();
-+ }
-+ }
-+
-+ /**
-+ * Creates the chunk task manager to work from the global workers. When {@link #close(boolean)} is invoked,
-+ * the global queue is not shutdown. If the global workers is configured to be disabled or use 0 threads, then
-+ * this chunk task manager will operate off of the world's chunk task queue.
-+ * @param world The world that this task manager is responsible for
-+ * @see 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 == ConcreteFileIOThread.FAILURE_VALUE;
-+ ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final ConcreteFileIOThread.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) {
-+ ConcreteFileIOThread.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);
-+
-+ ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final ConcreteFileIOThread.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
-+ ConcreteFileIOThread.Holder.INSTANCE.flush();
-+
-+ 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
-+ ConcreteFileIOThread.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
-+ ConcreteFileIOThread.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 3894b0434..054affc7d 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 = 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,6 +276,9 @@ public class ChunkProviderServer extends IChunkProvider {
- CompletableFuture> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag);
-
- if (!completablefuture.isDone()) { // Paper
-+ // Paper start - async chunk io // Paper start - async chunk loading
-+ this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
-+ // Paper end
- this.world.timings.chunkAwait.startTiming(); // Paper
- this.serverThreadQueue.awaitTasks(completablefuture::isDone);
- this.world.timings.chunkAwait.stopTiming(); // Paper
-diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
-index a02807411..61157b5dd 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)); // TODO obfhelpers
-+ NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); // TODO obfhelpers
-+
-+ // 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(); // TODO obfhelper
-+
-+ NBTTagList blockTickListSerialized;
-+ if (blockTickList instanceof ProtoChunkTickList || blockTickList instanceof TickListChunk) {
-+ blockTickListSerialized = null;
-+ } else {
-+ blockTickListSerialized = world.getBlockTickList().a(chunkPos); // TODO obfhelper
-+ }
-+
-+ TickList fluidTickList = chunk.o(); // TODO obfhelper
-+
-+ NBTTagList fluidTickListSerialized;
-+ if (fluidTickList instanceof ProtoChunkTickList || fluidTickList instanceof TickListChunk) {
-+ fluidTickListSerialized = null;
-+ } else {
-+ fluidTickListSerialized = world.getFluidTickList().a(chunkPos); // TODO obfhelper
-+ }
-+
-+ 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));
-+ nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i));
-+ } 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
-@@ -378,7 +497,11 @@ public class ChunkRegionLoader {
- 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));
- }
-@@ -388,7 +511,11 @@ public class ChunkRegionLoader {
- 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));
- }
-diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java
-index e324989b4..abb0d69d2 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 d521d25cf..84024e6ba 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 3f14392e6..00e92fa53 100644
---- a/src/main/java/net/minecraft/server/IChunkLoader.java
-+++ b/src/main/java/net/minecraft/server/IChunkLoader.java
-@@ -3,6 +3,10 @@ package net.minecraft.server;
- import com.mojang.datafixers.DataFixer;
- import java.io.File;
- import java.io.IOException;
-+// Paper start
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.CompletionException;
-+// Paper end
- import java.util.function.Supplier;
- import javax.annotation.Nullable;
-
-@@ -10,7 +14,9 @@ public class IChunkLoader extends RegionFileCache {
-
- protected final DataFixer b;
- @Nullable
-- private PersistentStructureLegacy a;
-+ private volatile PersistentStructureLegacy a; // Paper - async chunk loading
-+
-+ private final Object persistentDataLock = new Object(); // Paper
-
- public IChunkLoader(File file, DataFixer datafixer) {
- super(file);
-@@ -55,9 +61,34 @@ public class IChunkLoader extends RegionFileCache {
- NBTTagCompound level = nbttagcompound.getCompound("Level");
- if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
- ChunkProviderServer cps = (generatoraccess == null) ? null : ((WorldServer) generatoraccess).getChunkProvider();
-+ // Paper start - Async chunk loading
-+ CompletableFuture future = new CompletableFuture<>();
-+ Runnable runnable = () -> {
-+ try {
-+ // Paper end
- if (check(cps, pos.x - 1, pos.z) && check(cps, pos.x - 1, pos.z - 1) && check(cps, pos.x, pos.z - 1)) {
- level.setBoolean("LightPopulated", true);
- }
-+ // Paper start - Async chunk loading
-+ future.complete(null);
-+ } catch (IOException ex) {
-+ future.completeExceptionally(ex);
-+ }
-+ };
-+
-+ if (MinecraftServer.getServer().isMainThread()) {
-+ future.complete(null);
-+ runnable.run();
-+ } else {
-+ ((PlayerChunkMap)this).world.getChunkProvider().serverThreadQueue.addTask(runnable);
-+ }
-+
-+ try {
-+ future.join();
-+ } catch (CompletionException ex) {
-+ com.destroystokyo.paper.util.SneakyThrow.sneaky(ex.getCause());
-+ }
-+ // Paper end
- }
- }
- // CraftBukkit end
-@@ -65,11 +96,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 +122,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 23d1935dd..14f8b6104 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 443e0d627..c52295975 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -775,6 +775,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();
- }
-@@ -373,11 +374,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 +423,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.ConcreteFileIOThread.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(); // TODO async this
-+ 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 +490,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 +573,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 +610,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 +836,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.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z,
-+ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
-+
-+ if (async) {
-+ return true;
-+ }
-+
-+ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSaveIOWait.startTiming()) { // Paper
-+ Boolean successPoi = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, true);
-+ Boolean successChunk = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, false);
-+
-+ 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 +883,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 +1013,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.ConcreteFileIOThread.Holder.INSTANCE) {
-+ NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ false, true, true).join().chunkData;
-+
-+ if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.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.ConcreteFileIOThread.Holder.INSTANCE) {
-+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
-+
-+ Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.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 +1071,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.ConcreteFileIOThread.Holder.INSTANCE
-+ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false);
-+
-+ if (inProgressWrite != null) {
-+ return ChunkRegionLoader.getStatus(inProgressWrite);
-+ }
-+ // Paper end
-+
- RegionFile regionFile = this.getRegionFileIfLoaded(chunkPos);
-
- return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
- }
-
- public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException {
-+ // Paper start - async 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.ConcreteFileIOThread.Holder.INSTANCE
-+ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false);
-+
-+ if (inProgressWrite != null) {
-+ return ChunkRegionLoader.getStatus(inProgressWrite);
-+ }
-+ // Paper end
-+ synchronized (this) { // Paper - async io
- RegionFile regionFile = this.getRegionFile(chunkPos, false);
-
- if (!regionFile.chunkExists(chunkPos)) {
-@@ -931,18 +1118,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.ConcreteFileIOThread.Holder.INSTANCE
-+ .getPendingWrite(this.world, x, z, false);
-+
-+ if (inProgressWrite != null) {
-+ return net.minecraft.server.ChunkRegionLoader.getStatus(inProgressWrite);
-+ }
-+ // Paper end
-+ // variant of PlayerChunkMap#getChunkStatusOnDisk that does not load data off disk, but loads the region file
-+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z);
-+ synchronized (world.getChunkProvider().playerChunkMap) {
-+ net.minecraft.server.RegionFile file;
-+ try {
-+ file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false);
-+ } catch (IOException ex) {
-+ throw new RuntimeException(ex);
-+ }
-+
-+ return !file.chunkExists(chunkPos) ? ChunkStatus.EMPTY : file.getStatusIfCached(x, z);
-+ }
-+ }
-+ // Paper end
-+
- public IChunkAccess getUnloadingChunk(int chunkX, int chunkZ) {
- PlayerChunk chunkHolder = this.pendingUnload.get(ChunkCoordIntPair.pair(chunkX, chunkZ));
- return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow();
-@@ -1290,6 +1515,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 a8c8ace46..22144eb00 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 e0fdf5f90..d9283b36b 100644
---- a/src/main/java/net/minecraft/server/RegionFileCache.java
-+++ b/src/main/java/net/minecraft/server/RegionFileCache.java
-@@ -51,13 +51,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);
-
-@@ -344,7 +344,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 a343a7b31..7584174eb 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 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);
- }
-@@ -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);
- });
-
-@@ -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
-@@ -123,7 +128,7 @@ public class RegionFileSection extends RegionFi
- for (int l = 0; l < 16; ++l) {
- long i1 = SectionPosition.a(chunkcoordintpair, l).v();
- Optional optional = optionaldynamic.get(Integer.toString(l)).get().map((dynamic2) -> {
-- return (MinecraftSerializable) this.e.apply(() -> {
-+ return this.e.apply(() -> { // Paper - decompile fix
- this.a(i1);
- }, dynamic2);
- });
-@@ -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 9c114d2d3..e3150f85a 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 b0e6ad773..f6c95ae8c 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.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
-+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
-+ }
-+ }
-+ // Paper end
- this.a.a();
- }
-
-@@ -157,7 +181,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);
-@@ -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.ConcreteFileIOThread.Holder.INSTANCE) {
-+ NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE
-+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
-+ true, false, true).join().poiData;
-+
-+ if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.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.ConcreteFileIOThread.Holder.INSTANCE) {
-+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
-+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
-+
-+ Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.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) -> {
-@@ -215,7 +275,7 @@ public class VillagePlace extends RegionFileSection {
-
- private final Predicate super VillagePlaceRecord> d;
-
-- private Occupancy(Predicate predicate) {
-+ private Occupancy(Predicate super VillagePlaceRecord> predicate) { // Paper - decompile fix
- this.d = predicate;
- }
-
-diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index 2969fcd55..0e23eeb1f 100644
---- a/src/main/java/net/minecraft/server/WorldServer.java
-+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -1,9 +1,9 @@
- package net.minecraft.server;
-
- import co.aikar.timings.TimingHistory;
--import co.aikar.timings.Timings;
-
- import com.destroystokyo.paper.PaperWorldConfig;
-+import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
- import com.google.common.collect.Lists;
- import com.google.common.collect.Maps;
- import com.google.common.collect.Queues;
-@@ -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.ConcreteFileIOThread.ChunkDataController poiDataController = new com.destroystokyo.paper.io.ConcreteFileIOThread.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.ConcreteFileIOThread.ChunkDataController chunkDataController = new com.destroystokyo.paper.io.ConcreteFileIOThread.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 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 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 f59b2e49c..bc5b3fb5b 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
-@@ -2350,16 +2351,17 @@ public class CraftWorld implements World {
-
- @Override
- public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) {
-- // TODO placeholder
-- if (Bukkit.isPrimaryThread()) {
-- return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen));
-- } else {
-- CompletableFuture ret = new CompletableFuture<>();
-- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
-- ret.complete(getChunkAtGen(x, z, gen));
-- });
-- return ret;
-+ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z);
-+ if (immediate != null) {
-+ return CompletableFuture.completedFuture(immediate.bukkitChunk);
- }
-+
-+ CompletableFuture ret = new CompletableFuture<>();
-+ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> {
-+ ret.complete(chunk == null ? null : chunk.bukkitChunk);
-+ });
-+
-+ return ret;
- }
- // Paper end
-
---
-2.23.0.rc1
-
diff --git a/patches/server/0053-Rate-limit-packets-incoming-from-players.patch b/patches/server/0051-Rate-limit-packets-incoming-from-players.patch
similarity index 98%
rename from patches/server/0053-Rate-limit-packets-incoming-from-players.patch
rename to patches/server/0051-Rate-limit-packets-incoming-from-players.patch
index 514c041ce..b9110c6aa 100644
--- a/patches/server/0053-Rate-limit-packets-incoming-from-players.patch
+++ b/patches/server/0051-Rate-limit-packets-incoming-from-players.patch
@@ -1,4 +1,4 @@
-From d43e7fe139fadc1dc7f30a87ca3ad9e3cc654351 Mon Sep 17 00:00:00 2001
+From 1e007d01df31875d0e690aa5912d8015ba94a1bc Mon Sep 17 00:00:00 2001
From: Spottedleaf
Date: Mon, 11 Mar 2019 12:18:29 -0700
Subject: [PATCH] Rate limit packets incoming from players
@@ -12,7 +12,7 @@ Subject: [PATCH] Rate limit packets incoming from players
create mode 100644 src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-index 58e2a070..4fcecf40 100644
+index 58e2a0707..4fcecf409 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -392,6 +392,18 @@ public class PaperConfig {
@@ -36,7 +36,7 @@ index 58e2a070..4fcecf40 100644
//public static boolean asyncChunkGenThreadPerWorld = true; // Leave out for now until we can control this
diff --git a/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java b/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
new file mode 100644
-index 00000000..91c8c5f5
+index 000000000..91c8c5f53
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
@@ -0,0 +1,135 @@
@@ -176,7 +176,7 @@ index 00000000..91c8c5f5
+ }
+}
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
-index 96a785af..ff8b5e76 100644
+index 96a785af2..ff8b5e76f 100644
--- a/src/main/java/net/minecraft/server/NetworkManager.java
+++ b/src/main/java/net/minecraft/server/NetworkManager.java
@@ -150,6 +150,13 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
@@ -194,7 +194,7 @@ index 96a785af..ff8b5e76 100644
}
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
-index 520490d5..8aa8a672 100644
+index 520490d59..8aa8a672d 100644
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
@@ -140,6 +140,60 @@ public class PlayerConnection implements PacketListenerPlayIn {
@@ -259,5 +259,5 @@ index 520490d5..8aa8a672 100644
public void tick() {
this.syncPosition();
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0055-Don-t-recalculate-permissions-for-players-on-world-c.patch b/patches/server/0052-Don-t-recalculate-permissions-for-players-on-world-c.patch
similarity index 97%
rename from patches/server/0055-Don-t-recalculate-permissions-for-players-on-world-c.patch
rename to patches/server/0052-Don-t-recalculate-permissions-for-players-on-world-c.patch
index 92f9d1320..33fa9ae74 100644
--- a/patches/server/0055-Don-t-recalculate-permissions-for-players-on-world-c.patch
+++ b/patches/server/0052-Don-t-recalculate-permissions-for-players-on-world-c.patch
@@ -1,4 +1,4 @@
-From 95f61700633ff04a5734635b9a5299526728d2ee Mon Sep 17 00:00:00 2001
+From 25499ab7fd33b9297aae7e93cb07fd9d7f844c89 Mon Sep 17 00:00:00 2001
From: Tom
Date: Fri, 12 Jul 2019 07:59:35 -0500
Subject: [PATCH] Don't recalculate permissions for players on world change
diff --git a/patches/server/0052-Reduce-sync-loads.patch b/patches/server/0052-Reduce-sync-loads.patch
deleted file mode 100644
index 726f86806..000000000
--- a/patches/server/0052-Reduce-sync-loads.patch
+++ /dev/null
@@ -1,322 +0,0 @@
-From 783182a5e6d0caabd8833f2ab4b61f86669c82a0 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
----
- .../com/destroystokyo/paper/PaperCommand.java | 44 +++++
- .../paper/io/SyncLoadFinder.java | 172 ++++++++++++++++++
- .../minecraft/server/ChunkProviderServer.java | 1 +
- src/main/java/net/minecraft/server/World.java | 6 +-
- 4 files changed, 220 insertions(+), 3 deletions(-)
- create mode 100644 src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java
-
-diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
-index 8db92edc3..a37f11883 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 000000000..59aec1032
---- /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 64a899abc..b1bea8f90 100644
---- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
-+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-@@ -279,6 +279,7 @@ public class ChunkProviderServer extends IChunkProvider {
- // Paper start - async chunk io // Paper start - async chunk loading
- this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
- // 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);
- this.world.timings.chunkAwait.stopTiming(); // Paper
-diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
-index 9f657b01f..eb6929e2b 100644
---- a/src/main/java/net/minecraft/server/World.java
-+++ b/src/main/java/net/minecraft/server/World.java
-@@ -1252,7 +1252,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);
-@@ -1272,7 +1272,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);
-@@ -1294,7 +1294,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.20.1
-
diff --git a/patches/server/0056-Add-blacklist-option-for-grindstone.patch b/patches/server/0053-Add-blacklist-option-for-grindstone.patch
similarity index 96%
rename from patches/server/0056-Add-blacklist-option-for-grindstone.patch
rename to patches/server/0053-Add-blacklist-option-for-grindstone.patch
index 57394ad66..cb0640c02 100644
--- a/patches/server/0056-Add-blacklist-option-for-grindstone.patch
+++ b/patches/server/0053-Add-blacklist-option-for-grindstone.patch
@@ -1,4 +1,4 @@
-From 7f40cc38f5f6150023d2fddfe85838a04ae17059 Mon Sep 17 00:00:00 2001
+From 030ba500b2c8fa0f513540507b027038ed8a6860 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Thu, 1 Aug 2019 19:15:12 -0500
Subject: [PATCH] Add blacklist option for grindstone
@@ -10,7 +10,7 @@ Subject: [PATCH] Add blacklist option for grindstone
3 files changed, 35 insertions(+)
diff --git a/src/main/java/net/minecraft/server/ContainerGrindstone.java b/src/main/java/net/minecraft/server/ContainerGrindstone.java
-index ed88e208..0a5abd2e 100644
+index ed88e208d..0a5abd2e1 100644
--- a/src/main/java/net/minecraft/server/ContainerGrindstone.java
+++ b/src/main/java/net/minecraft/server/ContainerGrindstone.java
@@ -57,12 +57,24 @@ public class ContainerGrindstone extends Container {
@@ -51,7 +51,7 @@ index ed88e208..0a5abd2e 100644
Map map = EnchantmentManager.a(itemstack);
Iterator iterator = map.entrySet().iterator();
diff --git a/src/main/java/net/minecraft/server/ItemStack.java b/src/main/java/net/minecraft/server/ItemStack.java
-index 43e89b99..e538ba39 100644
+index 43e89b99b..e538ba399 100644
--- a/src/main/java/net/minecraft/server/ItemStack.java
+++ b/src/main/java/net/minecraft/server/ItemStack.java
@@ -538,6 +538,12 @@ public final class ItemStack {
@@ -68,7 +68,7 @@ index 43e89b99..e538ba39 100644
return this.getItem().f(this);
}
diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java
-index 20231143..ee83df5c 100644
+index 20231143e..ee83df5cc 100644
--- a/src/main/java/net/pl3x/purpur/PurpurConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java
@@ -1,6 +1,7 @@
@@ -98,5 +98,5 @@ index 20231143..ee83df5c 100644
private static void requireShiftToMount() {
requireShiftToMount = getBoolean("settings.mobs.require-shift-to-mount", requireShiftToMount);
--
-2.20.1
+2.23.0.rc1
diff --git a/patches/server/0057-Implement-AFK-API.patch b/patches/server/0054-Implement-AFK-API.patch
similarity index 97%
rename from patches/server/0057-Implement-AFK-API.patch
rename to patches/server/0054-Implement-AFK-API.patch
index 058ad16c3..6a09fb22c 100644
--- a/patches/server/0057-Implement-AFK-API.patch
+++ b/patches/server/0054-Implement-AFK-API.patch
@@ -1,4 +1,4 @@
-From d794df93ec583f24d4add8501c0d5afc8e2b276e Mon Sep 17 00:00:00 2001
+From 8abe41f131c0e11e484fa4d87bbbddd051e8510a Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Thu, 8 Aug 2019 15:29:15 -0500
Subject: [PATCH] Implement AFK API
@@ -49,10 +49,10 @@ index 2a943f316..8ee2e6c7f 100644
super(EntityTypes.PLAYER, world);
this.bY = ItemStack.a;
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
-index 622899d8f..1c4dd7b09 100644
+index f9469e9e7..9f495027e 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
-@@ -1593,8 +1593,51 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+@@ -1598,8 +1598,51 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
public void resetIdleTimer() {
this.cm = SystemUtils.getMonotonicMillis();
@@ -205,10 +205,10 @@ index 8aa8a672d..1ecadcebe 100644
if (from.getX() != Double.MAX_VALUE) {
Location oldTo = to.clone();
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index 0bbd28ed8..0cdb4f26a 100644
+index f996c2c24..a03b2395c 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -319,7 +319,7 @@ public class WorldServer extends World {
+@@ -318,7 +318,7 @@ public class WorldServer extends World {
}
if (this.C && this.players.stream().noneMatch((entityplayer) -> {
@@ -217,7 +217,7 @@ index 0bbd28ed8..0cdb4f26a 100644
})) {
this.C = false;
if (this.getGameRules().getBoolean(GameRules.DO_DAYLIGHT_CYCLE)) {
-@@ -586,7 +586,7 @@ public class WorldServer extends World {
+@@ -585,7 +585,7 @@ public class WorldServer extends World {
while (iterator.hasNext()) {
EntityPlayer entityplayer = (EntityPlayer) iterator.next();
diff --git a/patches/server/0054-implement-optional-per-player-mob-spawns.patch b/patches/server/0054-implement-optional-per-player-mob-spawns.patch
deleted file mode 100644
index 1708cfc94..000000000
--- a/patches/server/0054-implement-optional-per-player-mob-spawns.patch
+++ /dev/null
@@ -1,287 +0,0 @@
-From a588cde615cc358ee14a68296bd46ff6d278deed Mon Sep 17 00:00:00 2001
-From: kickash32
-Date: Tue, 11 Jun 2019 22:22:16 -0400
-Subject: [PATCH] implement optional per player mob spawns
-
----
- .../destroystokyo/paper/PaperWorldConfig.java | 5 ++++
- .../minecraft/server/ChunkProviderServer.java | 17 +++++++++--
- .../net/minecraft/server/EntityPlayer.java | 1 +
- .../net/minecraft/server/EntityTypes.java | 1 +
- .../net/minecraft/server/PlayerChunkMap.java | 14 +++++++--
- .../java/net/minecraft/server/PlayerMap.java | 1 +
- .../net/minecraft/server/SpawnerCreature.java | 29 ++++++++-----------
- .../net/minecraft/server/WorldServer.java | 26 +++++++++++++++++
- 8 files changed, 73 insertions(+), 21 deletions(-)
-
-diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
-index e7bbeef74..ac9883a4b 100644
---- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
-+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
-@@ -514,6 +514,11 @@ public class PaperWorldConfig {
- maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24);
- }
-
-+ public boolean perPlayerMobSpawns = false;
-+ private void perPlayerMobSpawns() {
-+ perPlayerMobSpawns = getBoolean("per-player-mob-spawns", false);
-+ }
-+
- public boolean countAllMobsForSpawning = false;
- private void countAllMobsForSpawning() {
- countAllMobsForSpawning = getBoolean("count-all-mobs-for-spawning", false);
-diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-index 46d38b924..4acc3c4c3 100644
---- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
-+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
-@@ -609,9 +609,22 @@ public class ChunkProviderServer extends IChunkProvider {
- // Paper start - only allow spawns upto the limit per chunk and update count afterwards
- int currEntityCount = object2intmap.getInt(enumcreaturetype);
- int difference = k1 - currEntityCount;
-+
-+ if (this.world.paperConfig.perPlayerMobSpawns) {
-+ int minDiff = Integer.MAX_VALUE;
-+ for(EntityPlayer entityplayer : playerChunkMap.getPlayersNear(chunk.getPos())) {
-+ if (entityplayer.isSpectator() || !entityplayer.affectsSpawning) { continue; }
-+ minDiff = Math.min(limit - this.world.getMobCountNear(entityplayer, enumcreaturetype), minDiff);
-+ }
-+ difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
-+ }
-+
- if (difference > 0) {
-- object2intmap.put(enumcreaturetype, currEntityCount + SpawnerCreature.spawnMobs(enumcreaturetype, world, chunk, blockposition, difference));
-- // Paper end
-+ SpawnerCreature.spawnMobs(enumcreaturetype, this.world, chunk, blockposition, difference, entity -> {
-+ this.world.updatePlayerMobCounts(entity);
-+ object2intmap.put(enumcreaturetype, currEntityCount + 1);
-+ });
-+ // Paper end
- }
- }
- }
-diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
-index d375069f3..622899d8f 100644
---- a/src/main/java/net/minecraft/server/EntityPlayer.java
-+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
-@@ -80,6 +80,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
- public boolean queueHealthUpdatePacket = false;
- public net.minecraft.server.PacketPlayOutUpdateHealth queuedHealthUpdatePacket;
- // Paper end
-+ public int[] mobCounts = new int[EnumCreatureType.values().length]; // Paper
-
- // CraftBukkit start
- public String displayName;
-diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java
-index 84aa1db72..7405d78c2 100644
---- a/src/main/java/net/minecraft/server/EntityTypes.java
-+++ b/src/main/java/net/minecraft/server/EntityTypes.java
-@@ -260,6 +260,7 @@ public class EntityTypes {
- return this.be;
- }
-
-+ public EnumCreatureType getEnumCreatureType(){ return this.e(); } // Paper - OBFHELPER
- public EnumCreatureType e() {
- return this.ba;
- }
-diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
-index 4c774e31d..e069c2f32 100644
---- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
-+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
-@@ -137,6 +137,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
- this.setViewDistance(i);
- }
-
-+ private static double squareDist(ChunkCoordIntPair chunkcoord, Entity entity) { return a(chunkcoord, entity); } // Paper - OBFHELPER
- private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
- double d0 = (double) (chunkcoordintpair.x * 16 + 8);
- double d1 = (double) (chunkcoordintpair.z * 16 + 8);
-@@ -146,6 +147,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
- return d2 * d2 + d3 * d3;
- }
-
-+ private static int distanceBetween(ChunkCoordIntPair chunkcoordintpair, EntityPlayer entityplayer) { return b(chunkcoordintpair, entityplayer, true); } // Paper - OBFHELPER
- private static int b(ChunkCoordIntPair chunkcoordintpair, EntityPlayer entityplayer, boolean flag) {
- int i;
- int j;
-@@ -1340,8 +1342,16 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
-
- }
-
-- @Override
-- public Stream a(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
-+ // Paper start
-+ public List getPlayersNear(ChunkCoordIntPair chunkcoordintpair) {
-+ List players = this.playerMap.getPlayers();
-+ players.removeIf(
-+ entityplayer -> distanceBetween(chunkcoordintpair, entityplayer) > this.viewDistance);
-+ return players;
-+ }
-+ // Paper end
-+
-+ @Override public Stream a(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
- return this.playerMap.a(chunkcoordintpair.pair()).filter((entityplayer) -> {
- int i = b(chunkcoordintpair, entityplayer, true);
-
-diff --git a/src/main/java/net/minecraft/server/PlayerMap.java b/src/main/java/net/minecraft/server/PlayerMap.java
-index f386c4e99..f8f35b0e0 100644
---- a/src/main/java/net/minecraft/server/PlayerMap.java
-+++ b/src/main/java/net/minecraft/server/PlayerMap.java
-@@ -10,6 +10,7 @@ public final class PlayerMap {
-
- public PlayerMap() {}
-
-+ public java.util.List getPlayers() { return new java.util.ArrayList<>(this.a.keySet()); } // Paper - Based on method below without streams
- public Stream a(long i) {
- return this.a.keySet().stream();
- }
-diff --git a/src/main/java/net/minecraft/server/SpawnerCreature.java b/src/main/java/net/minecraft/server/SpawnerCreature.java
-index 62fc61df2..b2a1999bb 100644
---- a/src/main/java/net/minecraft/server/SpawnerCreature.java
-+++ b/src/main/java/net/minecraft/server/SpawnerCreature.java
-@@ -18,18 +18,16 @@ public final class SpawnerCreature {
-
- // Paper start - add maxSpawns parameter and update counts
- public static void a(EnumCreatureType enumcreaturetype, World world, Chunk chunk, BlockPosition blockposition) {
-- spawnMobs(enumcreaturetype, world, chunk, blockposition, Integer.MAX_VALUE);
-+ spawnMobs(enumcreaturetype, world, chunk, blockposition, Integer.MAX_VALUE, entity -> {});
- }
--
-- public static int spawnMobs(EnumCreatureType enumcreaturetype, World world, Chunk chunk, BlockPosition blockposition, int maxSpawns) {
-- // Paper end
-+ public static void spawnMobs(EnumCreatureType enumcreaturetype, World world, Chunk chunk, BlockPosition blockposition, int maxSpawns, java.util.function.Consumer trackEntity) {
-+ // Paper end
- ChunkGenerator> chunkgenerator = world.getChunkProvider().getChunkGenerator();
-- int i = 0;
-+ int i = 0; // Paper - force diff on name change
- BlockPosition blockposition1 = getRandomPosition(world, chunk);
- int j = blockposition1.getX();
- int k = blockposition1.getY();
- int l = blockposition1.getZ();
-- int amountSpawned = 0; // Paper - keep track of mobs spawned
-
- if (k >= 1) {
- IBlockData iblockdata = world.getTypeIfLoadedAndInBounds(blockposition1); // Paper - don't load chunks for mob spawn
-@@ -81,11 +79,11 @@ public final class SpawnerCreature {
- // Purpur start
- if (entitytypes == EntityTypes.GIANT) {
- if (!net.pl3x.purpur.PurpurConfig.giantsNaturallySpawn) {
-- return amountSpawned;
-+ return;
- }
- } else if (entitytypes == EntityTypes.ILLUSIONER) {
- if (!net.pl3x.purpur.PurpurConfig.illusionersNaturallySpawn) {
-- return amountSpawned;
-+ return;
- }
- }
- // Purpur end
-@@ -107,7 +105,7 @@ public final class SpawnerCreature {
- );
- if (!event.callEvent()) {
- if (event.shouldAbortSpawn()) {
-- return amountSpawned; // Paper
-+ return;
- }
- ++i2;
- continue;
-@@ -126,7 +124,7 @@ public final class SpawnerCreature {
- } catch (Exception exception) {
- SpawnerCreature.LOGGER.warn("Failed to create mob", exception);
- ServerInternalException.reportInternalException(exception); // Paper
-- return amountSpawned; // Paper
-+ return;
- }
-
- entityinsentient.setPositionRotation((double) f, (double) k, (double) f1, world.random.nextFloat() * 360.0F, 0.0F);
-@@ -134,18 +132,16 @@ public final class SpawnerCreature {
- groupdataentity = entityinsentient.prepare(world, world.getDamageScaler(new BlockPosition(entityinsentient)), EnumMobSpawn.NATURAL, groupdataentity, (NBTTagCompound) null);
- // CraftBukkit start
- if (world.addEntity(entityinsentient, SpawnReason.NATURAL)) {
-- ++i;
-+ ++i; // Paper - force diff on name change
- ++i2;
- // Paper start - stop when limit is reached
-- ++amountSpawned;
-- }
-- if (amountSpawned >= maxSpawns) {
-- return amountSpawned;
-+ trackEntity.accept(entityinsentient);
- }
-+ if (i >= maxSpawns) { return; }
- // Paper end
- // CraftBukkit end
- if (i >= entityinsentient.dC()) {
-- return amountSpawned; // Paper
-+ return;
- }
-
- if (entityinsentient.c(i2)) {
-@@ -171,7 +167,6 @@ public final class SpawnerCreature {
-
- }
- }
-- return amountSpawned; // Paper
- }
-
- @Nullable
-diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
-index 0e23eeb1f..0bbd28ed8 100644
---- a/src/main/java/net/minecraft/server/WorldServer.java
-+++ b/src/main/java/net/minecraft/server/WorldServer.java
-@@ -70,6 +70,7 @@ public class WorldServer extends World {
- private boolean ticking;
- @Nullable
- private final MobSpawnerTrader mobSpawnerTrader;
-+ private Map> playerMobTypeMap; // Paper
-
- // CraftBukkit start
- private int tickPosition;
-@@ -966,6 +967,16 @@ public class WorldServer extends World {
- }
-
- public Object2IntMap l() {
-+ // Paper start
-+ if (this.paperConfig.perPlayerMobSpawns) {
-+ int maxI = EnumCreatureType.values().length;
-+ for(EntityPlayer player : this.players){
-+ for(int i = 0; i < maxI; i++) {
-+ player.mobCounts[i] = 0;
-+ }
-+ }
-+ }
-+ // Paper end
- Object2IntMap object2intmap = new Object2IntOpenHashMap();
- ObjectIterator objectiterator = this.entitiesById.values().iterator();
-
-@@ -990,6 +1001,7 @@ public class WorldServer extends World {
- entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
- continue;
- }
-+ updatePlayerMobCounts(entity);
- // Paper end
- object2intmap.mergeInt(enumcreaturetype, 1, Integer::sum);
- }
-@@ -998,6 +1010,20 @@ public class WorldServer extends World {
- return object2intmap;
- }
-
-+ // Paper start
-+ public void updatePlayerMobCounts(Entity entity) {
-+ if (!this.paperConfig.perPlayerMobSpawns) { return; }
-+ for (EntityPlayer player : ((ChunkProviderServer) this.chunkProvider).playerChunkMap.getPlayersNear(entity.getChunkAtLocation().getPos())) {
-+ EnumCreatureType enumType = entity.getEntityType().getEnumCreatureType();
-+ player.mobCounts[enumType.ordinal()]++;
-+ }
-+ }
-+
-+ public int getMobCountNear(EntityPlayer entityPlayer, EnumCreatureType enumCreatureType) {
-+ return entityPlayer.mobCounts[enumCreatureType.ordinal()];
-+ }
-+ // Paper end
-+
- @Override
- public boolean addEntity(Entity entity) {
- // CraftBukkit start
---
-2.23.0.rc1
-
diff --git a/patches/server/0058-Advancement-stuffs.patch b/patches/server/0055-Advancement-stuffs.patch
similarity index 92%
rename from patches/server/0058-Advancement-stuffs.patch
rename to patches/server/0055-Advancement-stuffs.patch
index 955973c30..3d843fd9e 100644
--- a/patches/server/0058-Advancement-stuffs.patch
+++ b/patches/server/0055-Advancement-stuffs.patch
@@ -1,4 +1,4 @@
-From 89cb55714c89430c7675fc909da2c0ebf0b07c87 Mon Sep 17 00:00:00 2001
+From 19494803caeaf400fd2e4f5a61c3077ec08a4eab Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sun, 11 Aug 2019 22:25:33 -0500
Subject: [PATCH] Advancement stuffs
diff --git a/patches/server/0059-Add-option-for-zombies-targetting-turtle-eggs.patch b/patches/server/0056-Add-option-for-zombies-targetting-turtle-eggs.patch
similarity index 97%
rename from patches/server/0059-Add-option-for-zombies-targetting-turtle-eggs.patch
rename to patches/server/0056-Add-option-for-zombies-targetting-turtle-eggs.patch
index 4160c8556..61b4f2e5d 100644
--- a/patches/server/0059-Add-option-for-zombies-targetting-turtle-eggs.patch
+++ b/patches/server/0056-Add-option-for-zombies-targetting-turtle-eggs.patch
@@ -1,4 +1,4 @@
-From ac6b964fcc587f744b965cfd03e45663667bb065 Mon Sep 17 00:00:00 2001
+From 966cdfa706801619c2a4849a6fe9fc91111b350c Mon Sep 17 00:00:00 2001
From: William Blake Galbreath
Date: Sat, 17 Aug 2019 15:27:09 -0500
Subject: [PATCH] Add option for zombies targetting turtle eggs