aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches
diff options
context:
space:
mode:
Diffstat (limited to 'patches')
-rw-r--r--patches/server/0009-MC-Utils.patch2
-rw-r--r--patches/server/0023-Timings-v2.patch2
-rw-r--r--patches/server/0066-Chunk-Save-Reattempt.patch6
-rw-r--r--patches/server/0082-Sanitise-RegionFileCache-and-make-configurable.patch4
-rw-r--r--patches/server/0165-PlayerNaturallySpawnCreaturesEvent.patch2
-rw-r--r--patches/server/0227-Add-Debug-Entities-option-to-debug-dupe-uuid-issues.patch2
-rw-r--r--patches/server/0312-Tracking-Range-Improvements.patch2
-rw-r--r--patches/server/0334-Prevent-Double-PlayerChunkMap-adds-crashing-server.patch2
-rw-r--r--patches/server/0345-Fire-PlayerJoinEvent-when-Player-is-actually-ready.patch2
-rw-r--r--patches/server/0612-Oprimise-map-impl-for-tracked-players.patch2
-rw-r--r--patches/server/0641-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch8
-rw-r--r--patches/server/0752-Fix-a-bunch-of-vanilla-bugs.patch2
-rw-r--r--patches/server/0781-Player-Entity-Tracking-Events.patch2
-rw-r--r--patches/server/0870-Configurable-entity-tracking-range-by-Y-coordinate.patch2
-rw-r--r--patches/server/0902-Don-t-check-if-we-can-see-non-visible-entities.patch2
-rw-r--r--patches/server/0931-Reduce-allocation-of-Vec3D-by-entity-tracker.patch2
-rw-r--r--patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch (renamed from patches/unapplied/server/0994-Rewrite-chunk-system.patch)28036
-rw-r--r--patches/server/0989-Rewrite-dataconverter-system.patch (renamed from patches/server/0988-Rewrite-dataconverter-system.patch)42
-rw-r--r--patches/server/0990-disable-forced-empty-world-ticks.patch (renamed from patches/server/0989-disable-forced-empty-world-ticks.patch)4
-rw-r--r--patches/server/0991-stubs.patch (renamed from patches/server/0990-stubs.patch)2
-rw-r--r--patches/unapplied/server/0993-Starlight.patch5429
21 files changed, 17523 insertions, 16034 deletions
diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch
index 111c5469d4..51373e4d16 100644
--- a/patches/server/0009-MC-Utils.patch
+++ b/patches/server/0009-MC-Utils.patch
@@ -6347,7 +6347,7 @@ index 5d4336210e11ee39521b4096a5f0874329053cdc..09d7b416c02eb13c506e9dc92d78e983
+ // Paper end
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 419a27a8bdc8adfeb6ea89e3bfe1838a80d75a33..ce0d22452171857e3cf070bf01450a7653ec7142 100644
+index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b8ef8fe7f 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -170,6 +170,62 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0023-Timings-v2.patch b/patches/server/0023-Timings-v2.patch
index 0ad75b6707..22c3be7c19 100644
--- a/patches/server/0023-Timings-v2.patch
+++ b/patches/server/0023-Timings-v2.patch
@@ -978,7 +978,7 @@ index d38ecbc208c34509eaf77751ac45d9ef51a5dce8..b51c3f8c485496734ea58c15377a1215
// CraftBukkit end
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index ce0d22452171857e3cf070bf01450a7653ec7142..6581566ca4e4fac0691e4f5851f8895d9ac7a38f 100644
+index 319f51eb8adde7584c74780ac0539f4b8ef8fe7f..ddadb0f13b96a39ec89cdaeea7bc02ee62ef2a06 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1,8 +1,10 @@
diff --git a/patches/server/0066-Chunk-Save-Reattempt.patch b/patches/server/0066-Chunk-Save-Reattempt.patch
index 0347b2117c..120ee75594 100644
--- a/patches/server/0066-Chunk-Save-Reattempt.patch
+++ b/patches/server/0066-Chunk-Save-Reattempt.patch
@@ -19,10 +19,10 @@ index b24e8255ab18eb5b2e4968aa62aa3d72ef33f0eb..12b7d50f49a2184aaf220a4a50a137b2
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index 4091d4d68b58bdefb2fdac1815e351d4f7c8a523..b7d0a48f38f0d8ae586012bb4e9a9faec21103c2 100644
+index 40f2f4d052add3b4270d29c843e49fb621e1bc8d..df099d4c7f101f50d40dae99b45c271b02712434 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -134,6 +134,11 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -134,6 +134,11 @@ public final class RegionFileStorage implements AutoCloseable {
protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
@@ -34,7 +34,7 @@ index 4091d4d68b58bdefb2fdac1815e351d4f7c8a523..b7d0a48f38f0d8ae586012bb4e9a9fae
if (nbt == null) {
regionfile.clear(pos);
-@@ -158,7 +163,18 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -158,7 +163,18 @@ public final class RegionFileStorage implements AutoCloseable {
dataoutputstream.close();
}
}
diff --git a/patches/server/0082-Sanitise-RegionFileCache-and-make-configurable.patch b/patches/server/0082-Sanitise-RegionFileCache-and-make-configurable.patch
index 8116e8a235..2693eaeb7c 100644
--- a/patches/server/0082-Sanitise-RegionFileCache-and-make-configurable.patch
+++ b/patches/server/0082-Sanitise-RegionFileCache-and-make-configurable.patch
@@ -11,10 +11,10 @@ The implementation uses a LinkedHashMap as an LRU cache (modified from HashMap).
The maximum size of the RegionFileCache is also made configurable.
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index b7d0a48f38f0d8ae586012bb4e9a9faec21103c2..7d4aa3d375bde32e0d2606346202929d481acad0 100644
+index df099d4c7f101f50d40dae99b45c271b02712434..491035aaefff4ee96435ec5d3f9417e28eae0796 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -39,7 +39,7 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -39,7 +39,7 @@ public final class RegionFileStorage implements AutoCloseable {
if (regionfile != null) {
return regionfile;
} else {
diff --git a/patches/server/0165-PlayerNaturallySpawnCreaturesEvent.patch b/patches/server/0165-PlayerNaturallySpawnCreaturesEvent.patch
index e297aaf103..7c35678f45 100644
--- a/patches/server/0165-PlayerNaturallySpawnCreaturesEvent.patch
+++ b/patches/server/0165-PlayerNaturallySpawnCreaturesEvent.patch
@@ -9,7 +9,7 @@ from triggering monster spawns on a server.
Also a highly more effecient way to blanket block spawns in a world
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 6581566ca4e4fac0691e4f5851f8895d9ac7a38f..c96346bd0207537899d266fe2c8f29a1663e10c3 100644
+index ddadb0f13b96a39ec89cdaeea7bc02ee62ef2a06..d04b69838c6f5fd1808782cacb31c6e00087bbac 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1101,7 +1101,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0227-Add-Debug-Entities-option-to-debug-dupe-uuid-issues.patch b/patches/server/0227-Add-Debug-Entities-option-to-debug-dupe-uuid-issues.patch
index a6502e83fc..d443d145a4 100644
--- a/patches/server/0227-Add-Debug-Entities-option-to-debug-dupe-uuid-issues.patch
+++ b/patches/server/0227-Add-Debug-Entities-option-to-debug-dupe-uuid-issues.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Add Debug Entities option to debug dupe uuid issues
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index c96346bd0207537899d266fe2c8f29a1663e10c3..e2f176d34443f0d1b00649efa45c65138042a015 100644
+index d04b69838c6f5fd1808782cacb31c6e00087bbac..96b7f0ac35a1e87c3f78a24180b207c32749fb71 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1321,6 +1321,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0312-Tracking-Range-Improvements.patch b/patches/server/0312-Tracking-Range-Improvements.patch
index ce3b2f8004..c72f88f8c8 100644
--- a/patches/server/0312-Tracking-Range-Improvements.patch
+++ b/patches/server/0312-Tracking-Range-Improvements.patch
@@ -8,7 +8,7 @@ Sets tracking range of watermobs to animals instead of misc and simplifies code
Also ignores Enderdragon, defaulting it to Mojang's setting
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index e2f176d34443f0d1b00649efa45c65138042a015..3784fbe3548727ab5ad8cfefef2d8d594a76123f 100644
+index 96b7f0ac35a1e87c3f78a24180b207c32749fb71..795c81c8f6fa59eded8b5a5084a8acb46d118fdb 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1613,6 +1613,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0334-Prevent-Double-PlayerChunkMap-adds-crashing-server.patch b/patches/server/0334-Prevent-Double-PlayerChunkMap-adds-crashing-server.patch
index 580844dd61..2f32768c21 100644
--- a/patches/server/0334-Prevent-Double-PlayerChunkMap-adds-crashing-server.patch
+++ b/patches/server/0334-Prevent-Double-PlayerChunkMap-adds-crashing-server.patch
@@ -7,7 +7,7 @@ Suspected case would be around the technique used in .stopRiding
Stack will identify any causer of this and warn instead of crashing.
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 3784fbe3548727ab5ad8cfefef2d8d594a76123f..5732aded2e4dbeea84dbe6ebac71c2ad5ce4729a 100644
+index 795c81c8f6fa59eded8b5a5084a8acb46d118fdb..1709821c73362b2ae54681ec1d59b40bfa9335b3 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1308,6 +1308,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0345-Fire-PlayerJoinEvent-when-Player-is-actually-ready.patch b/patches/server/0345-Fire-PlayerJoinEvent-when-Player-is-actually-ready.patch
index 1d485c4eb5..b9a47e9175 100644
--- a/patches/server/0345-Fire-PlayerJoinEvent-when-Player-is-actually-ready.patch
+++ b/patches/server/0345-Fire-PlayerJoinEvent-when-Player-is-actually-ready.patch
@@ -31,7 +31,7 @@ delays anymore.
public net.minecraft.server.level.ChunkMap addEntity(Lnet/minecraft/world/entity/Entity;)V
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 5732aded2e4dbeea84dbe6ebac71c2ad5ce4729a..d1247df5c51b0d377a27ea7cc5b5a2d1f1bf9b32 100644
+index 1709821c73362b2ae54681ec1d59b40bfa9335b3..68a1cc5f4f7f5997dfb7d40647e3e027c23ffb14 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1315,6 +1315,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0612-Oprimise-map-impl-for-tracked-players.patch b/patches/server/0612-Oprimise-map-impl-for-tracked-players.patch
index ad5326c8f4..2cadeeb2a0 100644
--- a/patches/server/0612-Oprimise-map-impl-for-tracked-players.patch
+++ b/patches/server/0612-Oprimise-map-impl-for-tracked-players.patch
@@ -7,7 +7,7 @@ Reference2BooleanOpenHashMap is going to have
better lookups than HashMap.
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index d1247df5c51b0d377a27ea7cc5b5a2d1f1bf9b32..cf7c7813d528429a18dc25051df7fc06dc159930 100644
+index 68a1cc5f4f7f5997dfb7d40647e3e027c23ffb14..77f064fb4437c1d98cf91dde98d4d88b28afa7c8 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1529,7 +1529,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0641-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch b/patches/server/0641-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
index 148e6899d1..80c053acc6 100644
--- a/patches/server/0641-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
+++ b/patches/server/0641-Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
@@ -44,10 +44,10 @@ index 12b7d50f49a2184aaf220a4a50a137b217c57124..f1237f6fd6414900ffbad0caee31aa83
public void close() throws IOException {
ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index 7d4aa3d375bde32e0d2606346202929d481acad0..36e914b26de070035f195f67c65ee1df0d10daf0 100644
+index 491035aaefff4ee96435ec5d3f9417e28eae0796..4c1212c6ef48594e766fa9e35a6e15916602d587 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -147,10 +147,17 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -147,10 +147,17 @@ public final class RegionFileStorage implements AutoCloseable {
try {
NbtIo.write(nbt, (DataOutput) dataoutputstream);
@@ -66,7 +66,7 @@ index 7d4aa3d375bde32e0d2606346202929d481acad0..36e914b26de070035f195f67c65ee1df
} catch (Throwable throwable1) {
throwable.addSuppressed(throwable1);
}
-@@ -158,10 +165,7 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -158,10 +165,7 @@ public final class RegionFileStorage implements AutoCloseable {
throw throwable;
}
@@ -78,7 +78,7 @@ index 7d4aa3d375bde32e0d2606346202929d481acad0..36e914b26de070035f195f67c65ee1df
}
// Paper start - Chunk save reattempt
return;
-@@ -208,4 +212,13 @@ public class RegionFileStorage implements AutoCloseable {
+@@ -208,4 +212,13 @@ public final class RegionFileStorage implements AutoCloseable {
public RegionStorageInfo info() {
return this.info;
}
diff --git a/patches/server/0752-Fix-a-bunch-of-vanilla-bugs.patch b/patches/server/0752-Fix-a-bunch-of-vanilla-bugs.patch
index 7af4dbba9b..eba9c58e8b 100644
--- a/patches/server/0752-Fix-a-bunch-of-vanilla-bugs.patch
+++ b/patches/server/0752-Fix-a-bunch-of-vanilla-bugs.patch
@@ -85,7 +85,7 @@ index 6854ca4d4fec2b4fa541c3fabf63787665572609..e7b444a10b244828827b3c66c5346520
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index cf7c7813d528429a18dc25051df7fc06dc159930..ef46d904fa49a779c235971883380b3e33e6dba1 100644
+index 77f064fb4437c1d98cf91dde98d4d88b28afa7c8..ccbd527803a2a4e911a01f815cc9c7ab785af836 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1091,7 +1091,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0781-Player-Entity-Tracking-Events.patch b/patches/server/0781-Player-Entity-Tracking-Events.patch
index 4b16731def..bdc7e8779e 100644
--- a/patches/server/0781-Player-Entity-Tracking-Events.patch
+++ b/patches/server/0781-Player-Entity-Tracking-Events.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Player Entity Tracking Events
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index ef46d904fa49a779c235971883380b3e33e6dba1..8eae75993ad60226a86456487f3b3a59999ab423 100644
+index ccbd527803a2a4e911a01f815cc9c7ab785af836..e2521e1a56df8dcb1de815e5973de952408d3b8b 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1601,7 +1601,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0870-Configurable-entity-tracking-range-by-Y-coordinate.patch b/patches/server/0870-Configurable-entity-tracking-range-by-Y-coordinate.patch
index 3c28b2c60f..a437b50f0f 100644
--- a/patches/server/0870-Configurable-entity-tracking-range-by-Y-coordinate.patch
+++ b/patches/server/0870-Configurable-entity-tracking-range-by-Y-coordinate.patch
@@ -6,7 +6,7 @@ Subject: [PATCH] Configurable entity tracking range by Y coordinate
Options to configure entity tracking by Y coordinate, also for each entity category.
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 8eae75993ad60226a86456487f3b3a59999ab423..38df456d3646c384d17ae9aec60c18fcd0651b4b 100644
+index e2521e1a56df8dcb1de815e5973de952408d3b8b..6c5557aad2455b79bb2adf8939eb9a6127ccc3c3 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1593,6 +1593,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0902-Don-t-check-if-we-can-see-non-visible-entities.patch b/patches/server/0902-Don-t-check-if-we-can-see-non-visible-entities.patch
index 43404ba162..109e1a443b 100644
--- a/patches/server/0902-Don-t-check-if-we-can-see-non-visible-entities.patch
+++ b/patches/server/0902-Don-t-check-if-we-can-see-non-visible-entities.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Don't check if we can see non-visible entities
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 38df456d3646c384d17ae9aec60c18fcd0651b4b..cf4517e57169856acd0782e5ced4eb8c045b8d78 100644
+index 6c5557aad2455b79bb2adf8939eb9a6127ccc3c3..469f1dcb22c06025681e727e281b5b53f2b21c1f 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1604,7 +1604,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/0931-Reduce-allocation-of-Vec3D-by-entity-tracker.patch b/patches/server/0931-Reduce-allocation-of-Vec3D-by-entity-tracker.patch
index 357d185f0a..86bd3e7f29 100644
--- a/patches/server/0931-Reduce-allocation-of-Vec3D-by-entity-tracker.patch
+++ b/patches/server/0931-Reduce-allocation-of-Vec3D-by-entity-tracker.patch
@@ -18,7 +18,7 @@ index a043ac10834562d357ef0b5aded2e916e2a0d056..74276c368016fcc4dbf9579b2ecbadc9
@VisibleForTesting
static long encode(double value) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index cf4517e57169856acd0782e5ced4eb8c045b8d78..6129720c9da217745fcd281186de7894597c267c 100644
+index 469f1dcb22c06025681e727e281b5b53f2b21c1f..2ce7da9707d7c1a48b5609ae51a516d599d7aee8 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1587,10 +1587,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/unapplied/server/0994-Rewrite-chunk-system.patch b/patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch
index 4df84d014c..d3df55cb84 100644
--- a/patches/unapplied/server/0994-Rewrite-chunk-system.patch
+++ b/patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch
@@ -1,3574 +1,3484 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <[email protected]>
-Date: Thu, 11 Mar 2021 02:32:30 -0800
-Subject: [PATCH] Rewrite chunk system
+Date: Fri, 14 Jun 2024 11:57:26 -0700
+Subject: [PATCH] Chunk System + Starlight from Moonrise
-Rebased patches:
+See https://github.com/Tuinity/Moonrise
-New player chunk loader system
-
-Make ChunkStatus.EMPTY not rely on the main thread for completion
-
-In order to do this, we need to push the POI consistency checks
-to a later status. Since FULL is the only other status that
-uses the main thread, it can go there.
-
-The consistency checks are only really for when a desync occurs,
-and so that delaying the check only matters when the chunk data
-has desync'd. As long as the desync is sorted before the
-chunk is full loaded (i.e before setBlock can occur on
-a chunk), it should not matter.
-
-This change is primarily due to behavioural changes
-in the chunk task queue brought by region threading -
-which is to split the queue into separate regions. As such,
-it is required that in order for the sync load to complete
-that the region owning the chunk drain and execute the task
-while ticking. However, that is not always possible in
-region threading. Thus, removing the main thread reliance allows
-the chunk to progress without requiring a tick thread.
-Specifically, this allows far sync loads (outside of a specific
-regions bounds) to occur without issue - namely with structure
-searching.
-
-Increase parallelism for neighbour writing chunk statuses
-
-Namely, everything after FEATURES. By creating a dependency
-chain indicating what chunks are in use, we can safely
-schedule completely independent tasks in parallel. This
-will allow the chunk system to scale beyond 10 threads
-per world.
-
-Properly cancel chunk load tasks that were not scheduled
-
-Since the chunk load task was not scheduled, the entity/poi load
-task fields will not be set, but the task complete counter
-will not be adjusted. Thus, the chunk load task will not complete.
-
-To resolve this, detect when the entity/poi tasks were not scheduled
-and decrement the task complete counter in such cases.
-
-Mark POI/Entity load tasks as completed before releasing scheduling lock
-
-It must be marked as completed during that lock hold since the
-waiters field is set to null. Thus, any other thread attempting
-a cancellation will fail to remove from waiters. Also, any
-other thread attempting to cancel may set the completed field
-to true which would cause accept() to fail as well.
-
-Completion was always designed to happen while holding the
-scheduling lock to prevent these race conditions. The code
-was originally set up to complete while not holding the
-scheduling lock to avoid invoking callbacks while holding the
-lock, however the access to the completion field was not
-considered.
-
-Resolve this by marking the callback as completed during the
-lock, but invoking the accept() function after releasing
-the lock. This will prevent any cancellation attempts to be
-blocked, and allow the current thread to complete the callback
-without any issues.
-
-Cache whether region files do not exist
-
-The repeated I/O of creating the directory for the regionfile
-or for checking if the file exists can be heavy in
-when pushing chunk generation extremely hard - as each chunk gen
-request may effectively go through to the I/O thread.
-
-Use coordinate-based locking to increase chunk system parallelism
-
-A significant overhead in Folia comes from the chunk system's
-locks, the ticket lock and the scheduling lock. The public
-test server, which had ~330 players, had signficant performance
-problems with these locks: ~80% of the time spent ticking
-was _waiting_ for the locks to free. Given that it used
-around 15 cores total at peak, this is a complete and utter loss
-of potential.
-
-To address this issue, I have replaced the ticket lock and scheduling
-lock with two ReentrantAreaLocks. The ReentrantAreaLock takes a
-shift, which is used internally to group positions into sections.
-This grouping is neccessary, as the possible radius of area that
-needs to be acquired for any given lock usage is up to 64. As such,
-the shift is critical to reduce the number of areas required to lock
-for any lock operation. Currently, it is set to a shift of 6, which
-is identical to the ticket level propagation shift (and, it must be
-at least the ticket level propagation shift AND the region shift).
-
-The chunk system locking changes required a complete rewrite of the
-chunk system tick, chunk system unload, and chunk system ticket level
-propagation - as all of the previous logic only works with a single
-global lock.
-
-This does introduce two other section shifts: the lock shift, and the
-ticket shift. The lock shift is simply what shift the area locks use,
-and the ticket shift represents the size of the ticket sections.
-Currently, these values are just set to the region shift for simplicity.
-However, they are not arbitrary: the lock shift must be at least the size
-of the ticket shift and must be at least the size of the region shift.
-The ticket shift must also be >= the ceil(log2(max ticket level source)).
-
-The chunk system's ticket propagator is now global state, instead of
-region state. This cleans up the logic for ticket levels significantly,
-and removes usage of the region lock in this area, but it also means
-that the addition of a ticket no longer creates a region. To alleviate
-the side effects of this change, the global tick thread now processes
-ticket level updates for each world every tick to guarantee eventual
-ticket level processing. The chunk system also provides a hook to
-process ticket level changes in a given _section_, so that the
-region queue can guarantee that after adding its reference counter
-that the region section is created/exists/wont be destroyed.
-
-The ticket propagator operates by updating the sources in a single ticket
-section, and propagating the updates to its 1 radius neighbours. This
-allows the ticket updates to occur in parallel or selectively (see above).
-Currently, the process ticket level update function operates by
-polling from a concurrent queue of sections to update and simply
-invoking the single section update logic. This allows the function
-to operate completely in parallel, provided the queue is ordered right.
-Additionally, this limits the area used in the ticket/scheduling lock
-when processing updates, which should massively increase parallelism compared
-to before.
-
-The chunk system ticket addition for expirable ticket types has been modified
-to no longer track exact tick deadlines, as this relies on what region the
-ticket is in. Instead, the chunk system tracks a map of
-lock section -> (chunk coordinate -> expire ticket count) and every ticket
-has been changed to have a removeDelay count that is decremented each tick.
-Each region searches its own sections to find tickets to try to expire.
-
-Chunk system unloading has been modified to track unloads by lock section.
-The ordering is determined by which section a chunk resides in.
-The unload process now removes from unload sections and processes
-the full unload stages (1, 2, 3) before moving to the next section, if possible.
-This allows the unload logic to only hold one lock section at a time for
-each lock, which is a massive parallelism increase.
-
-In stress testing, these changes lowered the locking overhead to only 5%
-from ~70%, which completely fix the original problem as described.
-
-== AT ==
-public net.minecraft.server.level.ChunkHolder pos
-public net.minecraft.server.level.ChunkMap overworldDataStorage
-public-f net.minecraft.world.level.chunk.storage.RegionFileStorage
-public net.minecraft.server.level.ChunkMap getPoiManager()Lnet/minecraft/world/entity/ai/village/poi/PoiManager;
-
-diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
new file mode 100644
-index 0000000000000000000000000000000000000000..4fd9a0cd8f1e6ae1a97e963dc7731a80bc6fac5b
+index 0000000000000000000000000000000000000000..ba68998f6ef57b24c72fd833bd7de440de9501cc
--- /dev/null
-+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
-@@ -0,0 +1,395 @@
-+package ca.spottedleaf.concurrentutil.lock;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import it.unimi.dsi.fastutil.HashCommon;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.concurrent.locks.LockSupport;
-+
-+public final class ReentrantAreaLock {
++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
++import net.minecraft.world.entity.Entity;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
+
-+ public final int coordinateShift;
++// list with O(1) remove & contains
+
-+ // aggressive load factor to reduce contention
-+ private final ConcurrentHashMap<Coordinate, Node> nodes = new ConcurrentHashMap<>(128, 0.2f);
++/**
++ * @author Spottedleaf
++ */
++public final class EntityList implements Iterable<Entity> {
+
-+ public ReentrantAreaLock(final int coordinateShift) {
-+ this.coordinateShift = coordinateShift;
++ protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
++ {
++ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
-+ public boolean isHeldByCurrentThread(final int x, final int z) {
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int sectionX = x >> shift;
-+ final int sectionZ = z >> shift;
++ protected static final Entity[] EMPTY_LIST = new Entity[0];
+
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
-+ final Node node = this.nodes.get(coordinate);
++ protected Entity[] entities = EMPTY_LIST;
++ protected int count;
+
-+ return node != null && node.thread == currThread;
++ public int size() {
++ return this.count;
+ }
+
-+ public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) {
-+ return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ public boolean contains(final Entity entity) {
++ return this.entityToIndex.containsKey(entity.getId());
+ }
+
-+ public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ if (fromX > toX || fromZ > toZ) {
-+ throw new IllegalArgumentException();
++ public boolean remove(final Entity entity) {
++ final int index = this.entityToIndex.remove(entity.getId());
++ if (index == Integer.MIN_VALUE) {
++ return false;
+ }
+
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int fromSectionX = fromX >> shift;
-+ final int fromSectionZ = fromZ >> shift;
-+ final int toSectionX = toX >> shift;
-+ final int toSectionZ = toZ >> shift;
++ // move the entity at the end to this index
++ final int endIndex = --this.count;
++ final Entity end = this.entities[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.entityToIndex.put(end.getId(), index); // update index
++ }
++ this.entities[index] = end;
++ this.entities[endIndex] = null;
++
++ return true;
++ }
+
-+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
-+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ));
++ public boolean add(final Entity entity) {
++ final int count = this.count;
++ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
+
-+ final Node node = this.nodes.get(coordinate);
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
+
-+ if (node == null || node.thread != currThread) {
-+ return false;
-+ }
-+ }
++ Entity[] list = this.entities;
++
++ if (list.length == count) {
++ // resize required
++ list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
++ list[count] = entity;
++ this.count = count + 1;
++
+ return true;
+ }
+
-+ public Node tryLock(final int x, final int z) {
-+ return this.tryLock(x, z, x, z);
++ public Entity getChecked(final int index) {
++ if (index < 0 || index >= this.count) {
++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
++ }
++ return this.entities[index];
+ }
+
-+ public Node tryLock(final int centerX, final int centerZ, final int radius) {
-+ return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ public Entity getUnchecked(final int index) {
++ return this.entities[index];
+ }
+
-+ public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ if (fromX > toX || fromZ > toZ) {
-+ throw new IllegalArgumentException();
-+ }
++ public Entity[] getRawData() {
++ return this.entities;
++ }
+
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int fromSectionX = fromX >> shift;
-+ final int fromSectionZ = fromZ >> shift;
-+ final int toSectionX = toX >> shift;
-+ final int toSectionZ = toZ >> shift;
++ public void clear() {
++ this.entityToIndex.clear();
++ Arrays.fill(this.entities, 0, this.count, null);
++ this.count = 0;
++ }
+
-+ final List<Coordinate> areaAffected = new ArrayList<>();
++ @Override
++ public Iterator<Entity> iterator() {
++ return new Iterator<Entity>() {
+
-+ final Node ret = new Node(this, areaAffected, currThread);
++ Entity lastRet;
++ int current;
+
-+ boolean failed = false;
++ @Override
++ public boolean hasNext() {
++ return this.current < EntityList.this.count;
++ }
+
-+ // try to fast acquire area
-+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
-+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ));
++ @Override
++ public Entity next() {
++ if (this.current >= EntityList.this.count) {
++ throw new NoSuchElementException();
++ }
++ return this.lastRet = EntityList.this.entities[this.current++];
++ }
+
-+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++ @Override
++ public void remove() {
++ final Entity lastRet = this.lastRet;
+
-+ if (prev == null) {
-+ areaAffected.add(coordinate);
-+ continue;
++ if (lastRet == null) {
++ throw new IllegalStateException();
+ }
++ this.lastRet = null;
+
-+ if (prev.thread != currThread) {
-+ failed = true;
-+ break;
-+ }
++ EntityList.this.remove(lastRet);
++ --this.current;
+ }
-+ }
++ };
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fcfbca333234c09f7c056bbfcd9ac8860b20a8db
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
+@@ -0,0 +1,125 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+ if (!failed) {
-+ return ret;
-+ }
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
++import java.util.Arrays;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.GlobalPalette;
+
-+ // failed, undo logic
-+ if (!areaAffected.isEmpty()) {
-+ for (int i = 0, len = areaAffected.size(); i < len; ++i) {
-+ final Coordinate key = areaAffected.get(i);
++public final class IBlockDataList {
+
-+ if (this.nodes.remove(key) != ret) {
-+ throw new IllegalStateException();
-+ }
-+ }
++ private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
++
++ // map of location -> (index | (location << 16) | (palette id << 32))
++ private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
++ {
++ this.map.defaultReturnValue(Long.MAX_VALUE);
++ }
+
-+ areaAffected.clear();
++ private static final long[] EMPTY_LIST = new long[0];
+
-+ // since we inserted, we need to drain waiters
-+ Thread unpark;
-+ while ((unpark = ret.pollOrBlockAdds()) != null) {
-+ LockSupport.unpark(unpark);
-+ }
-+ }
++ private long[] byIndex = EMPTY_LIST;
++ private int size;
+
-+ return null;
++ public static int getLocationKey(final int x, final int y, final int z) {
++ return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
+ }
+
-+ public Node lock(final int x, final int z) {
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int sectionX = x >> shift;
-+ final int sectionZ = z >> shift;
++ public static BlockState getBlockDataFromRaw(final long raw) {
++ return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
++ }
+
-+ final List<Coordinate> areaAffected = new ArrayList<>(1);
++ public static int getIndexFromRaw(final long raw) {
++ return (int)(raw & 0xFFFF);
++ }
+
-+ final Node ret = new Node(this, areaAffected, currThread);
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
++ public static int getLocationFromRaw(final long raw) {
++ return (int)((raw >>> 16) & 0xFFFF);
++ }
+
-+ for (long failures = 0L;;) {
-+ final Node park;
++ public static long getRawFromValues(final int index, final int location, final BlockState data) {
++ return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
++ }
+
-+ // try to fast acquire area
-+ {
-+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++ public static long setIndexRawValues(final long value, final int index) {
++ return value & ~(0xFFFF) | (index);
++ }
+
-+ if (prev == null) {
-+ areaAffected.add(coordinate);
-+ return ret;
-+ } else if (prev.thread != currThread) {
-+ park = prev;
-+ } else {
-+ // only one node we would want to acquire, and it's owned by this thread already
-+ return ret;
-+ }
-+ }
++ public long add(final int x, final int y, final int z, final BlockState data) {
++ return this.add(getLocationKey(x, y, z), data);
++ }
+
-+ ++failures;
++ public long add(final int location, final BlockState data) {
++ final long curr = this.map.get((short)location);
+
-+ if (failures > 128L && park.add(currThread)) {
-+ LockSupport.park();
-+ } else {
-+ // high contention, spin wait
-+ if (failures < 128L) {
-+ for (long i = 0; i < failures; ++i) {
-+ Thread.onSpinWait();
-+ }
-+ failures = failures << 1;
-+ } else if (failures < 1_200L) {
-+ LockSupport.parkNanos(1_000L);
-+ failures = failures + 1L;
-+ } else { // scale 0.1ms (100us) per failure
-+ Thread.yield();
-+ LockSupport.parkNanos(100_000L * failures);
-+ failures = failures + 1L;
-+ }
++ if (curr == Long.MAX_VALUE) {
++ final int index = this.size++;
++ final long raw = getRawFromValues(index, location, data);
++ this.map.put((short)location, raw);
++
++ if (index >= this.byIndex.length) {
++ this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
+ }
++
++ this.byIndex[index] = raw;
++ return raw;
++ } else {
++ final int index = getIndexFromRaw(curr);
++ final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
++
++ this.map.put((short)location, raw);
++
++ return raw;
+ }
+ }
+
-+ public Node lock(final int centerX, final int centerZ, final int radius) {
-+ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ public long remove(final int x, final int y, final int z) {
++ return this.remove(getLocationKey(x, y, z));
+ }
+
-+ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ if (fromX > toX || fromZ > toZ) {
-+ throw new IllegalArgumentException();
++ public long remove(final int location) {
++ final long ret = this.map.remove((short)location);
++ final int index = getIndexFromRaw(ret);
++ if (ret == Long.MAX_VALUE) {
++ return ret;
+ }
+
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int fromSectionX = fromX >> shift;
-+ final int fromSectionZ = fromZ >> shift;
-+ final int toSectionX = toX >> shift;
-+ final int toSectionZ = toZ >> shift;
-+
-+ if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) {
-+ return this.lock(fromX, fromZ);
++ // move the entry at the end to this index
++ final int endIndex = --this.size;
++ final long end = this.byIndex[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
+ }
++ this.byIndex[index] = end;
++ this.byIndex[endIndex] = 0L;
+
-+ final List<Coordinate> areaAffected = new ArrayList<>();
++ return ret;
++ }
+
-+ final Node ret = new Node(this, areaAffected, currThread);
++ public int size() {
++ return this.size;
++ }
+
-+ for (long failures = 0L;;) {
-+ Node park = null;
-+ boolean addedToArea = false;
-+ boolean alreadyOwned = false;
-+ boolean allOwned = true;
++ public long getRaw(final int index) {
++ return this.byIndex[index];
++ }
+
-+ // try to fast acquire area
-+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
-+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ));
++ public int getLocation(final int index) {
++ return getLocationFromRaw(this.getRaw(index));
++ }
+
-+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++ public BlockState getData(final int index) {
++ return getBlockDataFromRaw(this.getRaw(index));
++ }
+
-+ if (prev == null) {
-+ addedToArea = true;
-+ allOwned = false;
-+ areaAffected.add(coordinate);
-+ continue;
-+ }
++ public void clear() {
++ this.size = 0;
++ this.map.clear();
++ }
+
-+ if (prev.thread != currThread) {
-+ park = prev;
-+ alreadyOwned = true;
-+ break;
-+ }
-+ }
-+ }
++ public LongIterator getRawIterator() {
++ return this.map.values().iterator();
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c21e00812f1aaa1279834a0562d360d6b89e146c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
+@@ -0,0 +1,312 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+ if (park == null) {
-+ if (alreadyOwned && !allOwned) {
-+ throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas");
-+ }
-+ return ret;
-+ }
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
+
-+ // failed, undo logic
-+ if (addedToArea) {
-+ for (int i = 0, len = areaAffected.size(); i < len; ++i) {
-+ final Coordinate key = areaAffected.get(i);
++public final class IteratorSafeOrderedReferenceSet<E> {
+
-+ if (this.nodes.remove(key) != ret) {
-+ throw new IllegalStateException();
-+ }
-+ }
++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
+
-+ areaAffected.clear();
++ private final Reference2IntLinkedOpenHashMap<E> indexMap;
++ private int firstInvalidIndex = -1;
+
-+ // since we inserted, we need to drain waiters
-+ Thread unpark;
-+ while ((unpark = ret.pollOrBlockAdds()) != null) {
-+ LockSupport.unpark(unpark);
-+ }
-+ }
++ /* list impl */
++ private E[] listElements;
++ private int listSize;
+
-+ ++failures;
++ private final double maxFragFactor;
+
-+ if (failures > 128L && park.add(currThread)) {
-+ LockSupport.park(park);
-+ } else {
-+ // high contention, spin wait
-+ if (failures < 128L) {
-+ for (long i = 0; i < failures; ++i) {
-+ Thread.onSpinWait();
++ private int iteratorCount;
++
++ public IteratorSafeOrderedReferenceSet() {
++ this(16, 0.75f, 16, 0.2);
++ }
++
++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++ final double maxFragFactor) {
++ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
++ this.indexMap.defaultReturnValue(-1);
++ this.maxFragFactor = maxFragFactor;
++ this.listElements = (E[])new Object[arrayCapacity];
++ }
++
++ /*
++ public void check() {
++ int iterated = 0;
++ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
++ if (this.listElements != null) {
++ for (int i = 0; i < this.listSize; ++i) {
++ Object obj = this.listElements[i];
++ if (obj != null) {
++ iterated++;
++ if (!check.add((E)obj)) {
++ throw new IllegalStateException("contains duplicate");
++ }
++ if (!this.contains((E)obj)) {
++ throw new IllegalStateException("desync");
+ }
-+ failures = failures << 1;
-+ } else if (failures < 1_200L) {
-+ LockSupport.parkNanos(1_000L);
-+ failures = failures + 1L;
-+ } else { // scale 0.1ms (100us) per failure
-+ Thread.yield();
-+ LockSupport.parkNanos(100_000L * failures);
-+ failures = failures + 1L;
+ }
+ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++ }
+
-+ if (addedToArea) {
-+ // try again, so we need to allow adds so that other threads can properly block on us
-+ ret.allowAdds();
++ check.clear();
++ iterated = 0;
++ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final E element = iterator.next();
++ iterated++;
++ if (!check.add(element)) {
++ throw new IllegalStateException("contains duplicate (iterator is wrong)");
++ }
++ if (!this.contains(element)) {
++ throw new IllegalStateException("desync (iterator is wrong)");
+ }
+ }
-+ }
+
-+ public void unlock(final Node node) {
-+ if (node.lock != this) {
-+ throw new IllegalStateException("Unlock target lock mismatch");
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
+ }
++ }
++ */
++
++ private double getFragFactor() {
++ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
++ }
+
-+ final List<Coordinate> areaAffected = node.areaAffected;
++ public int createRawIterator() {
++ ++this.iteratorCount;
++ if (this.indexMap.isEmpty()) {
++ return -1;
++ } else {
++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++ }
++ }
+
-+ if (areaAffected.isEmpty()) {
-+ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters
-+ return;
++ public int advanceRawIterator(final int index) {
++ final E[] elements = this.listElements;
++ int ret = index + 1;
++ for (int len = this.listSize; ret < len; ++ret) {
++ if (elements[ret] != null) {
++ return ret;
++ }
+ }
+
-+ // remove from node map; allowing other threads to lock
-+ for (int i = 0, len = areaAffected.size(); i < len; ++i) {
-+ final Coordinate coordinate = areaAffected.get(i);
-+ if (this.nodes.remove(coordinate) != node) {
-+ throw new IllegalStateException();
++ return -1;
++ }
++
++ public void finishRawIterator() {
++ if (--this.iteratorCount == 0) {
++ if (this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
+ }
+ }
++ }
+
-+ Thread unpark;
-+ while ((unpark = node.pollOrBlockAdds()) != null) {
-+ LockSupport.unpark(unpark);
++ public boolean remove(final E element) {
++ final int index = this.indexMap.removeInt(element);
++ if (index >= 0) {
++ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
++ this.firstInvalidIndex = index;
++ }
++ if (this.listElements[index] != element) {
++ throw new IllegalStateException();
++ }
++ this.listElements[index] = null;
++ if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ //this.check();
++ return true;
+ }
++ return false;
+ }
+
-+ public static final class Node extends MultiThreadedQueue<Thread> {
++ public boolean contains(final E element) {
++ return this.indexMap.containsKey(element);
++ }
+
-+ private final ReentrantAreaLock lock;
-+ private final List<Coordinate> areaAffected;
-+ private final Thread thread;
-+ //private final Throwable WHO_CREATED_MY_ASS = new Throwable();
++ public boolean add(final E element) {
++ final int listSize = this.listSize;
+
-+ private Node(final ReentrantAreaLock lock, final List<Coordinate> areaAffected, final Thread thread) {
-+ this.lock = lock;
-+ this.areaAffected = areaAffected;
-+ this.thread = thread;
++ final int previous = this.indexMap.putIfAbsent(element, listSize);
++ if (previous != -1) {
++ return false;
+ }
+
-+ @Override
-+ public String toString() {
-+ return "Node{" +
-+ "areaAffected=" + this.areaAffected +
-+ ", thread=" + this.thread +
-+ '}';
++ if (listSize >= this.listElements.length) {
++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
+ }
-+ }
++ this.listElements[listSize] = element;
++ this.listSize = listSize + 1;
+
-+ private static final class Coordinate implements Comparable<Coordinate> {
-+
-+ public final long key;
++ //this.check();
++ return true;
++ }
+
-+ public Coordinate(final long key) {
-+ this.key = key;
++ private void defrag() {
++ if (this.firstInvalidIndex < 0) {
++ return; // nothing to do
+ }
+
-+ public Coordinate(final int x, final int z) {
-+ this.key = key(x, z);
++ if (this.indexMap.isEmpty()) {
++ Arrays.fill(this.listElements, 0, this.listSize, null);
++ this.listSize = 0;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ return;
+ }
+
-+ public static long key(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
++ final E[] backingArray = this.listElements;
+
-+ public static int x(final long key) {
-+ return (int)key;
++ int lastValidIndex;
++ java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
++
++ if (this.firstInvalidIndex == 0) {
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
++ lastValidIndex = 0;
++ } else {
++ lastValidIndex = this.firstInvalidIndex;
++ final E key = backingArray[lastValidIndex - 1];
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
++ @Override
++ public int getIntValue() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public int setValue(int i) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public E getKey() {
++ return key;
++ }
++ });
+ }
+
-+ public static int z(final long key) {
-+ return (int)(key >>> 32);
++ while (iterator.hasNext()) {
++ final Reference2IntMap.Entry<E> entry = iterator.next();
++
++ final int newIndex = lastValidIndex++;
++ backingArray[newIndex] = entry.getKey();
++ entry.setValue(newIndex);
+ }
+
-+ @Override
-+ public int hashCode() {
-+ return (int)HashCommon.mix(this.key);
++ // cleanup end
++ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
++ this.listSize = lastValidIndex;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ }
++
++ public E rawGet(final int index) {
++ return this.listElements[index];
++ }
++
++ public int size() {
++ // always returns the correct amount - listSize can be different
++ return this.indexMap.size();
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() {
++ return this.iterator(0);
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
++ ++this.iteratorCount;
++ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public java.util.Iterator<E> unsafeIterator() {
++ return this.unsafeIterator(0);
++ }
++ public java.util.Iterator<E> unsafeIterator(final int flags) {
++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public static interface Iterator<E> extends java.util.Iterator<E> {
++
++ public void finishedIterating();
++
++ }
++
++ private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
++
++ private final IteratorSafeOrderedReferenceSet<E> set;
++ private final boolean canFinish;
++ private final int maxIndex;
++ private int nextIndex;
++ private E pendingValue;
++ private boolean finished;
++ private E lastReturned;
++
++ private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
++ this.set = set;
++ this.canFinish = canFinish;
++ this.maxIndex = maxIndex;
+ }
+
+ @Override
-+ public boolean equals(final Object obj) {
-+ if (this == obj) {
++ public boolean hasNext() {
++ if (this.finished) {
++ return false;
++ }
++ if (this.pendingValue != null) {
+ return true;
+ }
+
-+ if (!(obj instanceof Coordinate other)) {
-+ return false;
++ final E[] elements = this.set.listElements;
++ int index, len;
++ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
++ final E element = elements[index];
++ if (element != null) {
++ this.pendingValue = element;
++ this.nextIndex = index + 1;
++ return true;
++ }
+ }
+
-+ return this.key == other.key;
++ this.nextIndex = index;
++ return false;
+ }
+
-+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain
-+ // is too large. So we should implement compareTo to help.
+ @Override
-+ public int compareTo(final Coordinate other) {
-+ return Long.compare(this.key, other.key);
++ public E next() {
++ if (!this.hasNext()) {
++ throw new NoSuchElementException();
++ }
++ final E ret = this.pendingValue;
++
++ this.pendingValue = null;
++ this.lastReturned = ret;
++
++ return ret;
+ }
+
+ @Override
-+ public String toString() {
-+ return "[" + x(this.key) + "," + z(this.key) + "]";
++ public void remove() {
++ final E lastReturned = this.lastReturned;
++ if (lastReturned == null) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.set.remove(lastReturned);
++ }
++
++ @Override
++ public void finishedIterating() {
++ if (this.finished || !this.canFinish) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.finished = true;
++ this.set.finishRawIterator();
+ }
+ }
+}
-diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
new file mode 100644
-index 0000000000000000000000000000000000000000..64b5803d002b2968841a5ddee987f98b72964e87
+index 0000000000000000000000000000000000000000..93e8c8134da8ee1a9b777c708f992922a1a7de8b
--- /dev/null
-+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java
-@@ -0,0 +1,217 @@
-+package ca.spottedleaf.concurrentutil.lock;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+@@ -0,0 +1,135 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import java.util.concurrent.locks.LockSupport;
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
+
-+// not concurrent, unlike ReentrantAreaLock
-+// no incorrect lock usage detection (acquiring intersecting areas)
-+// this class is nothing more than a performance reference for ReentrantAreaLock
-+public final class SyncReentrantAreaLock {
++public final class ReferenceList<E> implements Iterable<E> {
+
-+ private final int coordinateShift;
++ private final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
++ {
++ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
++ }
+
-+ // aggressive load factor to reduce contention
-+ private final Long2ReferenceOpenHashMap<Node> nodes = new Long2ReferenceOpenHashMap<>(128, 0.2f);
++ private static final Object[] EMPTY_LIST = new Object[0];
+
-+ public SyncReentrantAreaLock(final int coordinateShift) {
-+ this.coordinateShift = coordinateShift;
++ private E[] references;
++ private int count;
++
++ public ReferenceList() {
++ this((E[])EMPTY_LIST, 0);
+ }
+
-+ private static long key(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
++ public ReferenceList(final E[] array, final int count) {
++ this.references = array;
++ this.count = count;
+ }
+
-+ public Node lock(final int x, final int z) {
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int sectionX = x >> shift;
-+ final int sectionZ = z >> shift;
++ public int size() {
++ return this.count;
++ }
+
-+ final LongArrayList areaAffected = new LongArrayList();
++ public boolean contains(final E obj) {
++ return this.referenceToIndex.containsKey(obj);
++ }
+
-+ final Node ret = new Node(this, areaAffected, currThread);
++ public boolean remove(final E obj) {
++ final int index = this.referenceToIndex.removeInt(obj);
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
+
-+ final long coordinate = key(sectionX, sectionZ);
++ // move the object at the end to this index
++ final int endIndex = --this.count;
++ final E end = (E)this.references[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.referenceToIndex.put(end, index); // update index
++ }
++ this.references[index] = end;
++ this.references[endIndex] = null;
+
-+ for (long failures = 0L;;) {
-+ final Node park;
++ return true;
++ }
+
-+ synchronized (this) {
-+ // try to fast acquire area
-+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++ public boolean add(final E obj) {
++ final int count = this.count;
++ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
+
-+ if (prev == null) {
-+ areaAffected.add(coordinate);
-+ return ret;
-+ } else if (prev.thread != currThread) {
-+ park = prev;
-+ } else {
-+ // only one node we would want to acquire, and it's owned by this thread already
-+ return ret;
-+ }
-+ }
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
+
-+ ++failures;
++ E[] list = this.references;
+
-+ if (failures > 128L && park.add(currThread)) {
-+ LockSupport.park();
-+ } else {
-+ // high contention, spin wait
-+ if (failures < 128L) {
-+ for (long i = 0; i < failures; ++i) {
-+ Thread.onSpinWait();
-+ }
-+ failures = failures << 1;
-+ } else if (failures < 1_200L) {
-+ LockSupport.parkNanos(1_000L);
-+ failures = failures + 1L;
-+ } else { // scale 0.1ms (100us) per failure
-+ Thread.yield();
-+ LockSupport.parkNanos(100_000L * failures);
-+ failures = failures + 1L;
-+ }
-+ }
++ if (list.length == count) {
++ // resize required
++ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
-+ }
+
-+ public Node lock(final int centerX, final int centerZ, final int radius) {
-+ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
++ list[count] = obj;
++ this.count = count + 1;
++
++ return true;
+ }
+
-+ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ if (fromX > toX || fromZ > toZ) {
-+ throw new IllegalArgumentException();
++ public E getChecked(final int index) {
++ if (index < 0 || index >= this.count) {
++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+ }
++ return this.references[index];
++ }
+
-+ final Thread currThread = Thread.currentThread();
-+ final int shift = this.coordinateShift;
-+ final int fromSectionX = fromX >> shift;
-+ final int fromSectionZ = fromZ >> shift;
-+ final int toSectionX = toX >> shift;
-+ final int toSectionZ = toZ >> shift;
-+
-+ final LongArrayList areaAffected = new LongArrayList();
-+
-+ final Node ret = new Node(this, areaAffected, currThread);
++ public E getUnchecked(final int index) {
++ return this.references[index];
++ }
+
-+ for (long failures = 0L;;) {
-+ Node park = null;
-+ boolean addedToArea = false;
++ public Object[] getRawData() {
++ return this.references;
++ }
+
-+ synchronized (this) {
-+ // try to fast acquire area
-+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
-+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
-+ final long coordinate = key(currX, currZ);
++ public E[] getRawDataUnchecked() {
++ return this.references;
++ }
+
-+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
++ public void clear() {
++ this.referenceToIndex.clear();
++ Arrays.fill(this.references, 0, this.count, null);
++ this.count = 0;
++ }
+
-+ if (prev == null) {
-+ addedToArea = true;
-+ areaAffected.add(coordinate);
-+ continue;
-+ }
++ @Override
++ public Iterator<E> iterator() {
++ return new Iterator<>() {
++ private E lastRet;
++ private int current;
+
-+ if (prev.thread != currThread) {
-+ park = prev;
-+ break;
-+ }
-+ }
-+ }
++ @Override
++ public boolean hasNext() {
++ return this.current < ReferenceList.this.count;
++ }
+
-+ if (park == null) {
-+ return ret;
++ @Override
++ public E next() {
++ if (this.current >= ReferenceList.this.count) {
++ throw new NoSuchElementException();
+ }
++ return this.lastRet = ReferenceList.this.references[this.current++];
++ }
+
-+ // failed, undo logic
-+ if (!areaAffected.isEmpty()) {
-+ for (int i = 0, len = areaAffected.size(); i < len; ++i) {
-+ final long key = areaAffected.getLong(i);
++ @Override
++ public void remove() {
++ final E lastRet = this.lastRet;
+
-+ if (!this.nodes.remove(key, ret)) {
-+ throw new IllegalStateException();
-+ }
-+ }
++ if (lastRet == null) {
++ throw new IllegalStateException();
+ }
-+ }
++ this.lastRet = null;
+
-+ if (addedToArea) {
-+ areaAffected.clear();
-+ // since we inserted, we need to drain waiters
-+ Thread unpark;
-+ while ((unpark = ret.pollOrBlockAdds()) != null) {
-+ LockSupport.unpark(unpark);
-+ }
++ ReferenceList.this.remove(lastRet);
++ --this.current;
+ }
++ };
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..db92261a6cb3758391108361096417c61bc82cdc
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
+@@ -0,0 +1,117 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+ ++failures;
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.Comparator;
+
-+ if (failures > 128L && park.add(currThread)) {
-+ LockSupport.park();
-+ } else {
-+ // high contention, spin wait
-+ if (failures < 128L) {
-+ for (long i = 0; i < failures; ++i) {
-+ Thread.onSpinWait();
-+ }
-+ failures = failures << 1;
-+ } else if (failures < 1_200L) {
-+ LockSupport.parkNanos(1_000L);
-+ failures = failures + 1L;
-+ } else { // scale 0.1ms (100us) per failure
-+ Thread.yield();
-+ LockSupport.parkNanos(100_000L * failures);
-+ failures = failures + 1L;
-+ }
-+ }
++public final class SortedList<E> {
++
++ private static final Object[] EMPTY_LIST = new Object[0];
++
++ private Comparator<? super E> comparator;
++ private E[] elements;
++ private int count;
++
++ public SortedList(final Comparator<? super E> comparator) {
++ this((E[])EMPTY_LIST, comparator);
++ }
++
++ public SortedList(final E[] elements, final Comparator<? super E> comparator) {
++ this.elements = elements;
++ this.comparator = comparator;
++ }
++
++ // start, end are inclusive
++ private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> comparator,
++ int start, int end) {
++ while (start <= end) {
++ final int middle = (start + end) >>> 1;
+
-+ if (addedToArea) {
-+ // try again, so we need to allow adds so that other threads can properly block on us
-+ ret.allowAdds();
++ final E middleVal = elements[middle];
++
++ final int cmp = comparator.compare(element, middleVal);
++
++ if (cmp < 0) {
++ end = middle - 1;
++ } else {
++ start = middle + 1;
+ }
+ }
++
++ return start;
+ }
+
-+ public void unlock(final Node node) {
-+ if (node.lock != this) {
-+ throw new IllegalStateException("Unlock target lock mismatch");
-+ }
++ public int size() {
++ return this.count;
++ }
+
-+ final LongArrayList areaAffected = node.areaAffected;
++ public boolean isEmpty() {
++ return this.count == 0;
++ }
+
-+ if (areaAffected.isEmpty()) {
-+ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters
-+ return;
-+ }
++ public int add(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ this.count = count + 1;
++ final Comparator<? super E> comparator = this.comparator;
+
-+ // remove from node map; allowing other threads to lock
-+ synchronized (this) {
-+ for (int i = 0, len = areaAffected.size(); i < len; ++i) {
-+ final long coordinate = areaAffected.getLong(i);
-+ if (!this.nodes.remove(coordinate, node)) {
-+ throw new IllegalStateException();
-+ }
++ final int idx = insertIdx(elements, element, comparator, 0, count - 1);
++
++ if (count >= elements.length) {
++ // copy and insert at the same time
++ if (idx == count) {
++ this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ elements[count] = element;
++ return idx;
++ } else {
++ final E[] newElements = (E[])Array.newInstance(elements.getClass().getComponentType(), (int)Math.max(4L, count * 2L));
++ System.arraycopy(elements, 0, newElements, 0, idx);
++ newElements[idx] = element;
++ System.arraycopy(elements, idx, newElements, idx + 1, count - idx);
++ this.elements = newElements;
++ return idx;
++ }
++ } else {
++ if (idx == count) {
++ // no copy needed
++ elements[idx] = element;
++ return idx;
++ } else {
++ // shift elements down
++ System.arraycopy(elements, idx, elements, idx + 1, count - idx);
++ elements[idx] = element;
++ return idx;
+ }
+ }
++ }
+
-+ Thread unpark;
-+ while ((unpark = node.pollOrBlockAdds()) != null) {
-+ LockSupport.unpark(unpark);
++ public E get(final int idx) {
++ if (idx < 0 || idx >= this.count) {
++ throw new IndexOutOfBoundsException(idx);
+ }
++ return this.elements[idx];
+ }
+
-+ public static final class Node extends MultiThreadedQueue<Thread> {
+
-+ private final SyncReentrantAreaLock lock;
-+ private final LongArrayList areaAffected;
-+ private final Thread thread;
++ public E remove(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ final Comparator<? super E> comparator = this.comparator;
+
-+ private Node(final SyncReentrantAreaLock lock, final LongArrayList areaAffected, final Thread thread) {
-+ this.lock = lock;
-+ this.areaAffected = areaAffected;
-+ this.thread = thread;
++ final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
++ if (idx < 0) {
++ return null;
+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-index e0338db4d6fa359029ed5edeacc3646aa98701f5..c03dbb4a74d00d794be4139f0f7c4b5ff1b01d38 100644
---- a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-@@ -41,14 +41,14 @@ public final class StarLightInterface {
- protected final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
- protected final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
-
-- protected final LightQueue lightQueue = new LightQueue(this);
-+ public final io.papermc.paper.chunk.system.light.LightQueue lightQueue; // Paper - replace light queue
-
- protected final LayerLightEventListener skyReader;
- protected final LayerLightEventListener blockReader;
- protected final boolean isClientSide;
-
-- protected final int minSection;
-- protected final int maxSection;
-+ public final int minSection; // Paper - public
-+ public final int maxSection; // Paper - public
- protected final int minLightSection;
- protected final int maxLightSection;
-
-@@ -182,6 +182,7 @@ public final class StarLightInterface {
- StarLightInterface.this.sectionChange(pos, notReady);
- }
- };
-+ this.lightQueue = new io.papermc.paper.chunk.system.light.LightQueue(this); // Paper - replace light queue
- }
-
- public boolean hasSkyLight() {
-@@ -333,7 +334,7 @@ public final class StarLightInterface {
- return this.lightAccess;
- }
-
-- protected final SkyStarLightEngine getSkyLightEngine() {
-+ public final SkyStarLightEngine getSkyLightEngine() { // Paper - public
- if (this.cachedSkyPropagators == null) {
- return null;
- }
-@@ -348,7 +349,7 @@ public final class StarLightInterface {
- return ret;
- }
-
-- protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) {
-+ public final void releaseSkyLightEngine(final SkyStarLightEngine engine) { // Paper - public
- if (this.cachedSkyPropagators == null) {
- return;
- }
-@@ -357,7 +358,7 @@ public final class StarLightInterface {
- }
- }
-
-- protected final BlockStarLightEngine getBlockLightEngine() {
-+ public final BlockStarLightEngine getBlockLightEngine() { // Paper - public
- if (this.cachedBlockPropagators == null) {
- return null;
- }
-@@ -372,7 +373,7 @@ public final class StarLightInterface {
- return ret;
- }
-
-- protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) {
-+ public final void releaseBlockLightEngine(final BlockStarLightEngine engine) { // Paper - public
- if (this.cachedBlockPropagators == null) {
- return;
- }
-@@ -381,7 +382,7 @@ public final class StarLightInterface {
- }
- }
-
-- public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
-+ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks blockChange(final BlockPos pos) { // Paper - rewrite chunk system
- if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
- return null;
- }
-@@ -389,7 +390,7 @@ public final class StarLightInterface {
- return this.lightQueue.queueBlockChange(pos);
- }
-
-- public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { // Paper - rewrite chunk system
- if (this.world == null) { // empty world
- return null;
- }
-@@ -519,57 +520,15 @@ public final class StarLightInterface {
- }
-
- public void scheduleChunkLight(final ChunkPos pos, final Runnable run) {
-- this.lightQueue.queueChunkLighting(pos, run);
-+ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue
- }
-
- public void removeChunkTasks(final ChunkPos pos) {
-- this.lightQueue.removeChunk(pos);
-+ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue
- }
-
- public void propagateChanges() {
-- if (this.lightQueue.isEmpty()) {
-- return;
-- }
--
-- final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-- final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
--
-- try {
-- LightQueue.ChunkTasks task;
-- while ((task = this.lightQueue.removeFirstTask()) != null) {
-- if (task.lightTasks != null) {
-- for (final Runnable run : task.lightTasks) {
-- run.run();
-- }
-- }
--
-- final long coordinate = task.chunkCoordinate;
-- final int chunkX = CoordinateUtils.getChunkX(coordinate);
-- final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
--
-- final Set<BlockPos> positions = task.changedPositions;
-- final Boolean[] sectionChanges = task.changedSectionSet;
--
-- if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-- skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-- }
-- if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-- blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-- }
--
-- if (skyEngine != null && task.queuedEdgeChecksSky != null) {
-- skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky);
-- }
-- if (blockEngine != null && task.queuedEdgeChecksBlock != null) {
-- blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock);
-- }
--
-- task.onComplete.complete(null);
-- }
-- } finally {
-- this.releaseSkyLightEngine(skyEngine);
-- this.releaseBlockLightEngine(blockEngine);
-- }
-+ throw new UnsupportedOperationException("No longer implemented, task draining is now performed by the light thread"); // Paper - replace light queue
- }
-
- public static final class LightQueue {
-diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-index 2f0d9b953802dee821cfde82d22b0567cce8ee91..22687667ec69a954261e55e59261286ac1b8b8cd 100644
---- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
-@@ -59,6 +59,16 @@ public class WorldTimingsHandler {
-
- public final Timing miscMobSpawning;
-
-+ public final Timing poiUnload;
-+ public final Timing chunkUnload;
-+ public final Timing poiSaveDataSerialization;
-+ public final Timing chunkSave;
-+ public final Timing chunkSaveDataSerialization;
-+ public final Timing chunkSaveIOWait;
-+ public final Timing chunkUnloadPrepareSave;
-+ public final Timing chunkUnloadPOISerialization;
-+ public final Timing chunkUnloadDataSave;
+
- public WorldTimingsHandler(Level server) {
- String name = ((PrimaryLevelData) server.getLevelData()).getLevelName() + " - ";
-
-@@ -112,6 +122,16 @@ public class WorldTimingsHandler {
-
-
- miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc");
++ final int last = this.count - 1;
++ this.count = last;
+
-+ poiUnload = Timings.ofSafe(name + "Chunk unload - POI");
-+ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk");
-+ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization");
-+ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk");
-+ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization");
-+ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait");
-+ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare");
-+ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization");
-+ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization");
- }
-
- public static Timing getTickList(ServerLevel worldserver, String timingsType) {
-diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-index cff2f04409fab9abca87ceec85a551e1d59f9e7d..e3f56908cc8a9c3f4580def50fcfdc61bd495a71 100644
---- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-@@ -32,192 +32,41 @@ public final class ChunkSystem {
- }
-
- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
-- level.chunkSource.mainThreadProcessor.execute(run);
-+ level.chunkTaskScheduler.scheduleChunkTask(chunkX, chunkZ, run, priority); // Paper - rewrite chunk system
- }
-
- public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
- final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
- final Consumer<ChunkAccess> onComplete) {
-- if (gen) {
-- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-- return;
-- }
-- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
-- if (chunk == null) {
-- onComplete.accept(null);
-- } else {
-- if (chunk.getStatus().isOrAfter(toStatus)) {
-- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-- } else {
-- onComplete.accept(null);
-- }
-- }
-- });
-+ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system
- }
-
-- static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
--
-- private static long chunkLoadCounter = 0L;
-+ // Paper - rewrite chunk system
- public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
- final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-- if (!Bukkit.isPrimaryThread()) {
-- scheduleChunkTask(level, chunkX, chunkZ, () -> {
-- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-- }, priority);
-- return;
-- }
--
-- final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
-- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
--
-- if (addTicket) {
-- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-- }
-- level.chunkSource.runDistanceManagerUpdates();
--
-- final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
-- try {
-- if (onComplete != null) {
-- onComplete.accept(chunk);
-- }
-- } catch (final ThreadDeath death) {
-- throw death;
-- } catch (final Throwable thr) {
-- LOGGER.error("Exception handling chunk load callback", thr);
-- SneakyThrow.sneaky(thr);
-- } finally {
-- if (addTicket) {
-- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
-- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-- }
-- }
-- };
--
-- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (holder == null || holder.getTicketLevel() > minLevel) {
-- loadCallback.accept(null);
-- return;
-- }
--
-- final CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap);
--
-- if (loadFuture.isDone()) {
-- loadCallback.accept(loadFuture.join().left().orElse(null));
-- return;
-- }
--
-- loadFuture.whenCompleteAsync((final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> {
-- if (thr != null) {
-- loadCallback.accept(null);
-- return;
-- }
-- loadCallback.accept(either.left().orElse(null));
-- }, (final Runnable r) -> {
-- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
-- });
-+ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system
- }
-
- public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
- final FullChunkStatus toStatus, final boolean addTicket,
- final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
-- // This method goes unused until the chunk system rewrite
-- if (toStatus == FullChunkStatus.INACCESSIBLE) {
-- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
-- }
--
-- if (!Bukkit.isPrimaryThread()) {
-- scheduleChunkTask(level, chunkX, chunkZ, () -> {
-- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-- }, priority);
-- return;
-- }
--
-- final int minLevel = 33 - (toStatus.ordinal() - 1);
-- final int radius = toStatus.ordinal() - 1;
-- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
--
-- if (addTicket) {
-- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-- }
-- level.chunkSource.runDistanceManagerUpdates();
--
-- final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
-- try {
-- if (onComplete != null) {
-- onComplete.accept(chunk);
-- }
-- } catch (final ThreadDeath death) {
-- throw death;
-- } catch (final Throwable thr) {
-- LOGGER.error("Exception handling chunk load callback", thr);
-- SneakyThrow.sneaky(thr);
-- } finally {
-- if (addTicket) {
-- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
-- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
-- }
-- }
-- };
--
-- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (holder == null || holder.getTicketLevel() > minLevel) {
-- loadCallback.accept(null);
-- return;
-- }
--
-- final CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> tickingState;
-- switch (toStatus) {
-- case FULL: {
-- tickingState = holder.getFullChunkFuture();
-- break;
-- }
-- case BLOCK_TICKING: {
-- tickingState = holder.getTickingChunkFuture();
-- break;
-- }
-- case ENTITY_TICKING: {
-- tickingState = holder.getEntityTickingChunkFuture();
-- break;
-- }
-- default: {
-- throw new IllegalStateException("Cannot reach here");
-- }
-- }
--
-- if (tickingState.isDone()) {
-- loadCallback.accept(tickingState.join().left().orElse(null));
-- return;
-- }
--
-- tickingState.whenCompleteAsync((final Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> {
-- if (thr != null) {
-- loadCallback.accept(null);
-- return;
-- }
-- loadCallback.accept(either.left().orElse(null));
-- }, (final Runnable r) -> {
-- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
-- });
-+ level.chunkTaskScheduler.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system
- }
-
- public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
-- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
-+ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system
- }
-
- public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
-- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
-+ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system
- }
-
- public static int getVisibleChunkHolderCount(final ServerLevel level) {
-- return level.chunkSource.chunkMap.visibleChunkMap.size();
-+ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system
- }
-
- public static int getUpdatingChunkHolderCount(final ServerLevel level) {
-- return level.chunkSource.chunkMap.updatingChunkMap.size();
-+ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system
- }
-
- public static boolean hasAnyChunkHolders(final ServerLevel level) {
-@@ -244,26 +93,32 @@ public final class ChunkSystem {
-
- public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.playerChunk = holder;
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL;
- }
-
- public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
--
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.INACCESSIBLE;
- }
-
- public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().tickingChunks.add(chunk);
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING;
-+ chunk.level.chunkSource.chunkMap.tickingGenerated.incrementAndGet();
- }
-
- public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().tickingChunks.remove(chunk);
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL;
- }
-
- public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().entityTickingChunks.add(chunk);
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING;
- }
-
- public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
-+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING;
- }
-
- public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
-@@ -271,23 +126,15 @@ public final class ChunkSystem {
- }
-
- public static int getSendViewDistance(final ServerPlayer player) {
-- return getLoadViewDistance(player);
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
- }
-
- public static int getLoadViewDistance(final ServerPlayer player) {
-- final ServerLevel level = player.serverLevel();
-- if (level == null) {
-- return Bukkit.getViewDistance();
-- }
-- return level.chunkSource.chunkMap.getPlayerViewDistance(player);
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getLoadViewDistance(player);
- }
-
- public static int getTickViewDistance(final ServerPlayer player) {
-- final ServerLevel level = player.serverLevel();
-- if (level == null) {
-- return Bukkit.getSimulationDistance();
-- }
-- return level.chunkSource.chunkMap.distanceManager.simulationDistance;
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
- }
-
- private ChunkSystem() {
-diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ee58c67cb2bd78159cce19ec75f13dc6168a0e7a
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
-@@ -0,0 +1,1375 @@
-+package io.papermc.paper.chunk.system;
++ final E ret = elements[idx];
+
-+import ca.spottedleaf.concurrentutil.collection.SRSWLinkedQueue;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
-+import io.papermc.paper.configuration.GlobalConfiguration;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import io.papermc.paper.util.player.SingleUserAreaMap;
-+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongComparator;
-+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import net.minecraft.network.protocol.Packet;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
-+import net.minecraft.server.level.ChunkTrackingView;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.server.network.PlayerChunkSender;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.GameRules;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.entity.Player;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayDeque;
-+import java.util.Arrays;
-+import java.util.Objects;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicLong;
++ System.arraycopy(elements, idx + 1, elements, idx, last - idx);
+
-+public class RegionizedPlayerChunkLoader {
++ elements[last] = null;
+
-+ // expected that this list returns for a given radius, the set of chunks ordered
-+ // by manhattan distance
-+ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][];
-+ static {
-+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
-+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
-+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
-+ }
++ return ret;
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..62caf61a4b0b7ebc764006ea8bbd0274594d9f4a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ private static void expandQuadrants(final CustomLongArray input, final int size) {
-+ final int len = input.size();
-+ final long[] array = input.elements();
++import it.unimi.dsi.fastutil.ints.Int2IntFunction;
+
-+ int writeIndex = size - 1;
-+ for (int i = len - 1; i >= 0; --i) {
-+ final long key = array[i];
-+ final int chunkX = CoordinateUtils.getChunkX(key);
-+ final int chunkZ = CoordinateUtils.getChunkZ(key);
++import java.util.Arrays;
+
-+ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) {
-+ throw new IllegalStateException();
-+ }
++public class Int2IntArraySortedMap {
+
-+ // Q4
-+ if (chunkZ != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ);
-+ }
-+ // Q3
-+ if (chunkX != 0 && chunkZ != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ);
-+ }
-+ // Q2
-+ if (chunkX != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ);
-+ }
++ protected int[] key;
++ protected int[] val;
++ protected int size;
+
-+ array[writeIndex--] = key;
++ public Int2IntArraySortedMap() {
++ this.key = new int[8];
++ this.val = new int[8];
++ }
++
++ public int put(final int key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ input.forceSize(size);
++ this.key[insert] = key;
++ this.val[insert] = value;
+
-+ if (writeIndex != -1) {
-+ throw new IllegalStateException();
-+ }
++ return 0;
+ }
+
-+ private static long[] generateBFSOrder(final int radius) {
-+ // by using only the first quadrant, we can reduce the total element size by 4 when spreading
-+ final CustomLongArray[] byDistance = makeQ1BFS(radius);
++ public int computeIfAbsent(final int key, final Int2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating
-+ // this also means we are minimising locality
-+ // but, we need to maintain sorted order by manhatten distance
++ this.key[insert] = key;
+
-+ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other
-+ for (int i = 0, len = byDistance.length; i < len; ++i) {
-+ final CustomLongArray points = byDistance[i];
-+ final int expectedSize = getDistanceSize(i, radius);
++ return this.val[insert] = producer.apply(key);
++ }
+
-+ final CustomLongArray spread = spread(points, expectedSize);
-+ // add in Q2, Q3, Q4
-+ expandQuadrants(spread, expectedSize);
++ public int get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
++ }
++ return this.val[index];
++ }
+
-+ byDistance[i] = spread;
++ public int getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
+ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fea9e8ba7caaf6259614090d4f872619470d32f9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+@@ -0,0 +1,74 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ // now, rebuild the list so that it still maintains manhatten distance order
-+ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1));
++import java.util.Arrays;
++import java.util.function.IntFunction;
+
-+ for (final CustomLongArray dist : byDistance) {
-+ ret.addAll(dist);
-+ }
++public class Int2ObjectArraySortedMap<V> {
+
-+ return ret.elements();
++ protected int[] key;
++ protected V[] val;
++ protected int size;
++
++ public Int2ObjectArraySortedMap() {
++ this.key = new int[8];
++ this.val = (V[])new Object[8];
+ }
+
-+ public static final TicketType<Long> REGION_PLAYER_TICKET = TicketType.create("region_player_ticket", Long::compareTo);
++ public V put(final int key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
+
-+ public static final int MIN_VIEW_DISTANCE = 2;
-+ public static final int MAX_VIEW_DISTANCE = 32;
++ this.key[insert] = key;
++ this.val[insert] = value;
+
-+ public static final int TICK_TICKET_LEVEL = 31;
-+ public static final int GENERATED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL);
-+ public static final int LOADED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.EMPTY);
++ return null;
++ }
+
-+ public static final record ViewDistances(
-+ int tickViewDistance,
-+ int loadViewDistance,
-+ int sendViewDistance
-+ ) {
-+ public ViewDistances setTickViewDistance(final int distance) {
-+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
++ public V computeIfAbsent(final int key, final IntFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
+ }
-+
-+ public ViewDistances setLoadViewDistance(final int distance) {
-+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
+
++ this.key[insert] = key;
+
-+ public ViewDistances setSendViewDistance(final int distance) {
-+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
-+ }
++ return this.val[insert] = producer.apply(key);
+ }
+
-+ public static int getAPITickViewDistance(final Player player) {
-+ return getAPITickViewDistance(((CraftPlayer)player).getHandle());
++ public V get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
++ }
++ return this.val[index];
+ }
+
-+ public static int getAPITickViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = (ServerLevel)player.level();
-+ final PlayerChunkLoaderData data = player.chunkLoader;
-+ if (data == null) {
-+ return level.playerChunkLoader.getAPITickDistance();
++ public V getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1);
++ return this.val[insert];
+ }
-+ return data.lastTickDistance;
++ return this.val[index];
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c077ca606934e9f13da3a8e2a194f82a99fe9ae9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2IntFunction;
++
++import java.util.Arrays;
++
++public class Long2IntArraySortedMap {
+
-+ public static int getAPIViewDistance(final Player player) {
-+ return getAPIViewDistance(((CraftPlayer)player).getHandle());
++ protected long[] key;
++ protected int[] val;
++ protected int size;
++
++ public Long2IntArraySortedMap() {
++ this.key = new long[8];
++ this.val = new int[8];
+ }
+
-+ public static int getAPIViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = (ServerLevel)player.level();
-+ final PlayerChunkLoaderData data = player.chunkLoader;
-+ if (data == null) {
-+ return level.playerChunkLoader.getAPIViewDistance();
++ public int put(final long key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
+ }
-+ // view distance = load distance + 1
-+ return data.lastLoadDistance - 1;
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return 0;
+ }
+
-+ public static int getLoadViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = (ServerLevel)player.level();
-+ final PlayerChunkLoaderData data = player.chunkLoader;
-+ if (data == null) {
-+ return level.playerChunkLoader.getAPIViewDistance();
++ public int computeIfAbsent(final long key, final Long2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
+ }
-+ // view distance = load distance + 1
-+ return data.lastLoadDistance - 1;
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
+ }
+
-+ public static int getAPISendViewDistance(final Player player) {
-+ return getAPISendViewDistance(((CraftPlayer)player).getHandle());
++ public int get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
++ }
++ return this.val[index];
+ }
+
-+ public static int getAPISendViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = (ServerLevel)player.level();
-+ final PlayerChunkLoaderData data = player.chunkLoader;
-+ if (data == null) {
-+ return level.playerChunkLoader.getAPISendViewDistance();
++ public int getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
+ }
-+ return data.lastSendDistance;
++ return this.val[index];
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b24d037af5709196b66c79c692e1814cd5b20e49
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+@@ -0,0 +1,76 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ private final ServerLevel world;
++import java.util.Arrays;
++import java.util.function.LongFunction;
+
-+ public RegionizedPlayerChunkLoader(final ServerLevel world) {
-+ this.world = world;
++public class Long2ObjectArraySortedMap<V> {
++
++ protected long[] key;
++ protected V[] val;
++ protected int size;
++
++ public Long2ObjectArraySortedMap() {
++ this.key = new long[8];
++ this.val = (V[])new Object[8];
+ }
+
-+ public void addPlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
-+ if (!player.isRealPlayer) {
-+ return;
++ public V put(final long key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ if (player.chunkLoader != null) {
-+ throw new IllegalStateException("Player is already added to player chunk loader");
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
++ }
++
++ public V computeIfAbsent(final long key, final LongFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
+ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
++ this.key[insert] = key;
+
-+ player.chunkLoader = loader;
-+ loader.add();
++ return this.val[insert] = producer.apply(key);
+ }
+
-+ public void updatePlayer(final ServerPlayer player) {
-+ final PlayerChunkLoaderData loader = player.chunkLoader;
-+ if (loader != null) {
-+ loader.update();
++ public V get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
+ }
++ return this.val[index];
+ }
+
-+ public void removePlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
-+ if (!player.isRealPlayer) {
-+ return;
++ public V getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? null : this.val[insert];
+ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aa86882bb7b0712f29d7344009093c0e7a81be84
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+@@ -0,0 +1,48 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ final PlayerChunkLoaderData loader = player.chunkLoader;
++import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
++import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
+
-+ if (loader == null) {
-+ return;
-+ }
++public final class SynchronisedLong2BooleanMap {
++ private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
++ private final int limit;
+
-+ loader.remove();
-+ player.chunkLoader = null;
++ public SynchronisedLong2BooleanMap(final int limit) {
++ this.limit = limit;
+ }
+
-+ public void setSendDistance(final int distance) {
-+ this.world.setSendViewDistance(distance);
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLastBoolean();
++ }
+ }
+
-+ public void setLoadDistance(final int distance) {
-+ this.world.setLoadViewDistance(distance);
++ public boolean remove(final long key) {
++ synchronized (this.map) {
++ return this.map.remove(key);
++ }
+ }
+
-+ public void setTickDistance(final int distance) {
-+ this.world.setTickViewDistance(distance);
-+ }
++ // note:
++ public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ }
+
-+ // Note: follow the player chunk loader so everything stays consistent...
-+ public int getAPITickDistance() {
-+ final ViewDistances distances = this.world.getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
-+ return tickViewDistance;
-+ }
++ final boolean put = ifAbsent.get(key);
+
-+ public int getAPIViewDistance() {
-+ final ViewDistances distances = this.world.getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
-+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ this.map.putAndMoveToFirst(key, put);
+
-+ // loadDistance = api view distance + 1
-+ return loadDistance - 1;
++ this.purgeEntries();
++
++ return put;
++ }
+ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..dbb51afc6cefe0071fe3ddcd2c1109f2755c3b4d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+@@ -0,0 +1,47 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ public int getAPISendViewDistance() {
-+ final ViewDistances distances = this.world.getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance);
-+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
-+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
-+ loadDistance, -1, -1, distances.sendViewDistance
-+ );
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import java.util.function.BiFunction;
+
-+ return sendViewDistance;
-+ }
++public final class SynchronisedLong2ObjectMap<V> {
++ private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
++ private final int limit;
+
-+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
-+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
++ public SynchronisedLong2ObjectMap(final int limit) {
++ this.limit = limit;
+ }
+
-+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerChunkLoaderData loader = player.chunkLoader;
-+ if (loader == null) {
-+ return false;
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLast();
+ }
-+
-+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
-+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerChunkLoaderData loader = player.chunkLoader;
-+ if (loader == null) {
-+ return false;
++ public V get(final long key) {
++ synchronized (this.map) {
++ return this.map.getAndMoveToFirst(key);
+ }
++ }
+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
-+ return true;
-+ }
-+ }
++ public V put(final long key, final V value) {
++ synchronized (this.map) {
++ final V ret = this.map.putAndMoveToFirst(key, value);
++ this.purgeEntries();
++ return ret;
+ }
-+
-+ return false;
+ }
+
-+ public void tick() {
-+ TickThread.ensureTickThread("Cannot tick player chunk loader async");
-+ long currTime = System.nanoTime();
-+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
-+ final PlayerChunkLoaderData loader = player.chunkLoader;
-+ if (loader == null || loader.world != this.world) {
-+ // not our problem anymore
-+ continue;
-+ }
-+ loader.update(); // can't invoke plugin logic
-+ loader.updateQueues(currTime);
++ public V compute(final long key, final BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
++ synchronized (this.map) {
++ // first, compute the value - if one is added, it will be at the last entry
++ this.map.compute(key, remappingFunction);
++ // move the entry to first, just in case it was added at last
++ final V ret = this.map.getAndMoveToFirst(key);
++ // now purge the last entries
++ this.purgeEntries();
++
++ return ret;
+ }
+ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9c0eff9017b24bb65b1029cefb5d0bfcb9beff01
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+@@ -0,0 +1,75 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ public static final class PlayerChunkLoaderData {
++public final class AllocatingRateLimiter {
+
-+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
-+ private final long id = ID_GENERATOR.incrementAndGet();
-+ private final Long idBoxed = Long.valueOf(this.id);
++ // max difference granularity in ns
++ private final long maxGranularity;
+
-+ private static final long MAX_RATE = 10_000L;
++ private double allocation = 0.0;
++ private long lastAllocationUpdate;
++ // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error)
++ // over any time period using take regardless of the number of take calls or the intervals between the take calls
++ // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3
++ private double takeCarry = 0.0;
++ private long lastTakeUpdate;
+
-+ private final ServerPlayer player;
-+ private final ServerLevel world;
++ public AllocatingRateLimiter(final long maxGranularity) {
++ this.maxGranularity = maxGranularity;
++ }
+
-+ private int lastChunkX = Integer.MIN_VALUE;
-+ private int lastChunkZ = Integer.MIN_VALUE;
++ public void reset(final long time) {
++ this.allocation = 0.0;
++ this.lastAllocationUpdate = time;
++ this.takeCarry = 0.0;
++ this.lastTakeUpdate = time;
++ }
+
-+ private int lastSendDistance = Integer.MIN_VALUE;
-+ private int lastLoadDistance = Integer.MIN_VALUE;
-+ private int lastTickDistance = Integer.MIN_VALUE;
++ // rate in units/s, and time in ns
++ public void tickAllocation(final long time, final double rate, final double maxAllocation) {
++ final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate);
++ this.lastAllocationUpdate = time;
+
-+ private int lastSentChunkCenterX = Integer.MIN_VALUE;
-+ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
++ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
++ }
+
-+ private int lastSentChunkRadius = Integer.MIN_VALUE;
-+ private int lastSentSimulationDistance = Integer.MIN_VALUE;
++ public long previewAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
+
-+ private boolean canGenerateChunks = true;
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
+
-+ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
-+ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
+
-+ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
-+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
-+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
-+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
-+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
-+ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
-+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
-+ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
-+ LOADED_TICKET_LEVEL,
-+ LOADED_TICKET_LEVEL,
-+ GENERATED_TICKET_LEVEL,
-+ GENERATED_TICKET_LEVEL,
-+ TICK_TICKET_LEVEL
-+ };
-+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
-+ {
-+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
++ return (long)Math.floor(this.takeCarry + take);
++ }
++
++ // rate in units/s, and time in ns
++ public long takeAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
+ }
+
-+ // rate limiting
-+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter();
-+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter();
-+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter();
++ double ret = this.takeCarry;
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++ this.lastTakeUpdate = time;
+
-+ // queues
-+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
-+ final int c1x = CoordinateUtils.getChunkX(c1);
-+ final int c1z = CoordinateUtils.getChunkZ(c1);
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
+
-+ final int c2x = CoordinateUtils.getChunkX(c2);
-+ final int c2z = CoordinateUtils.getChunkZ(c2);
++ ret += take;
++ this.allocation -= take;
+
-+ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
-+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
++ final long retInteger = (long)Math.floor(ret);
++ this.takeCarry = ret - (double)retInteger;
+
-+ return Integer.compare(
-+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
-+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
-+ );
-+ };
-+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ return retInteger;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..460e27ab0506c83a28934800ee74ee886d4b025e
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+@@ -0,0 +1,297 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ private volatile boolean removed;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+
-+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
-+ this.world = world;
-+ this.player = player;
-+ }
++public final class Delayed26WayDistancePropagator3D {
+
-+ private void flushDelayedTicketOps() {
-+ if (this.delayedTicketOps.isEmpty()) {
-+ return;
-+ }
-+ this.world.chunkTaskScheduler.chunkHolderManager.performTicketUpdates(this.delayedTicketOps);
-+ this.delayedTicketOps.clear();
-+ }
++ // this map is considered "stale" unless updates are propagated.
++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
+
-+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
-+ this.delayedTicketOps.addLast(op);
-+ }
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
+
-+ private void sendChunk(final int chunkX, final int chunkZ) {
-+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ PlayerChunkSender.sendChunk(this.player.connection, this.world, this.world.getChunkIfLoaded(chunkX, chunkZ));
-+ return;
-+ }
-+ throw new IllegalStateException();
-+ }
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
+
-+ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
-+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ return;
-+ }
-+ this.sendUnloadChunkRaw(chunkX, chunkZ);
-+ }
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
+
-+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
-+ PlayerChunkSender.dropChunkStatic(this.player, new ChunkPos(chunkX, chunkZ));
-+ }
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
+
-+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we only care about remove
-+ }
++ }
+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ parameter.sendUnloadChunk(chunkX, chunkZ);
-+ }
-+ };
-+ private final SingleUserAreaMap<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we only care about remove
-+ }
++ protected final LevelChangeCallback changeCallback;
+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
-+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
-+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
-+ return;
-+ }
++ public Delayed26WayDistancePropagator3D() {
++ this(null);
++ }
+
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunk,
-+ TicketType.UNKNOWN, level, new ChunkPos(chunkX, chunkZ),
-+ REGION_PLAYER_TICKET, level, parameter.idBoxed
-+ ));
-+ }
-+ };
-+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we will detect ticking chunks when we try to load them
-+ }
++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
-+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
-+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
-+ return;
-+ }
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
+
-+ // Since we are possibly downgrading the ticket level, we add an unknown ticket so that
-+ // the level is kept until tick().
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunk,
-+ TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ),
-+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
-+ ));
-+ // keep chunk at new generated level
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
-+ chunk,
-+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
-+ ));
-+ }
-+ };
++ public int getLevel(final int x, final int y, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
+
-+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
-+ final int sendRadius) {
-+ // expect sendRadius to be = 1 + target viewable radius
-+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true);
-+ }
++ public void setSource(final int x, final int y, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
++ }
+
-+ private static int getClientViewDistance(final ServerPlayer player) {
-+ final Integer vd = player.requestedViewDistance();
-+ return vd == null ? -1 : Math.max(0, vd.intValue());
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
+ }
+
-+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) {
-+ return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance;
-+ }
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
+
-+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
-+ final int worldLoadViewDistance) {
-+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
+ }
+
-+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
-+ final int playerSendViewDistance, final int worldSendViewDistance) {
-+ return Math.min(
-+ loadViewDistance - 1,
-+ playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
-+ );
-+ }
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
+
-+ private Packet<?> updateClientChunkRadius(final int radius) {
-+ this.lastSentChunkRadius = radius;
-+ return new ClientboundSetChunkCacheRadiusPacket(radius);
-+ }
++ public void removeSource(final int x, final int y, final int z) {
++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
+
-+ private Packet<?> updateClientSimulationDistance(final int distance) {
-+ this.lastSentSimulationDistance = distance;
-+ return new ClientboundSetSimulationDistancePacket(distance);
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
+ }
++ }
+
-+ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
-+ this.lastSentChunkCenterX = chunkX;
-+ this.lastSentChunkCenterZ = chunkZ;
-+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
++ // queues used for BFS propagating levels
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
+ }
-+
-+ private boolean canPlayerGenerateChunks() {
-+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
++ }
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
+ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
+
-+ private double getMaxChunkLoadRate() {
-+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
-+ }
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
+
-+ private double getMaxChunkGenRate() {
-+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
-+ }
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
+
-+ private double getMaxChunkSendRate() {
-+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
+ }
+
-+ private long getMaxChunkLoads() {
-+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
-+ if (configLimit == 0L) {
-+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
-+ configLimit = Math.max(5L, radiusChunks / 5L);
-+ } else if (configLimit < 0L) {
-+ configLimit = Integer.MAX_VALUE;
-+ } // else: use the value configured
-+ configLimit = configLimit - this.loadingQueue.size();
++ boolean ret = false;
+
-+ return configLimit;
-+ }
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
+
-+ private long getMaxChunkGenerates() {
-+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
-+ if (configLimit == 0L) {
-+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
-+ configLimit = Math.max(5L, radiusChunks / 5L);
-+ } else if (configLimit < 0L) {
-+ configLimit = Integer.MAX_VALUE;
-+ } // else: use the value configured
-+ configLimit = configLimit - this.generatingQueue.size();
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
+
-+ return configLimit;
-+ }
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
+
-+ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
-+ final int dx = this.lastChunkX - chunkX;
-+ final int dz = this.lastChunkZ - chunkZ;
-+ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded(
-+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
-+ );
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
+ }
+
-+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
-+ final int dx = this.lastChunkX - chunkX;
-+ final int dz = this.lastChunkZ - chunkZ;
-+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
-+ }
++ this.updatedSources.clear();
+
-+ void updateQueues(final long time) {
-+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
-+ if (this.removed) {
-+ throw new IllegalStateException("Ticking removed player chunk loader");
-+ }
-+ // update rate limits
-+ final double loadRate = this.getMaxChunkLoadRate();
-+ final double genRate = this.getMaxChunkGenRate();
-+ final double sendRate = this.getMaxChunkSendRate();
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
+
-+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
-+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
-+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
+
-+ // try to progress chunk loads
-+ while (!this.loadingQueue.isEmpty()) {
-+ final long pendingLoadChunk = this.loadingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
-+ final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ);
-+ if (pending == null) {
-+ // nothing to do here
-+ break;
-+ }
-+ // chunk has loaded, so we can take it out of the queue
-+ this.loadingQueue.dequeueLong();
++ return ret;
++ }
+
-+ // try to move to generate queue
-+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
-+ if (prev != CHUNK_TICKET_STAGE_LOADING) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
+ }
+
-+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
-+ this.genQueue.enqueue(pendingLoadChunk);
-+ } // else: don't want to generate, so just leave it loaded
-+ }
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
+
-+ // try to push more chunk loads
-+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
-+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
-+ if (maxLoadsThisTick > 0) {
-+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
-+ for (int i = 0; i < maxLoadsThisTick; ++i) {
-+ final long chunk = this.loadQueue.dequeueLong();
-+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
-+ if (prev != CHUNK_TICKET_STAGE_NONE) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
+ }
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addOp(
-+ chunk,
-+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ chunks.add(chunk);
-+ this.loadingQueue.enqueue(chunk);
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
+ }
+
-+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
-+ this.flushDelayedTicketOps();
-+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
-+ // load - only generate ticket levels start anything, but they start generation...
-+ // propagate levels
-+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
-+ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
-+
-+ if (this.removed) {
-+ // process ticket updates may invoke plugin logic, which may remove this player
-+ return;
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
+ }
+
-+ for (int i = 0; i < maxLoadsThisTick; ++i) {
-+ final long queuedLoadChunk = chunks.getLong(i);
-+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
-+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
-+ this.world.chunkTaskScheduler.scheduleChunkLoad(
-+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
-+ );
-+ if (this.removed) {
-+ return;
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
+ }
+ }
+ }
++ }
++ }
+
-+ // try to progress chunk generations
-+ while (!this.generatingQueue.isEmpty()) {
-+ final long pendingGenChunk = this.generatingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
-+ final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ);
-+ if (pending == null) {
-+ // nothing to do here
-+ break;
-+ }
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
+
-+ // chunk has generated, so we can take it out of queue
-+ this.generatingQueue.dequeueLong();
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
+
-+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
-+ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
+ }
+
-+ // try to move to send queue
-+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
-+ this.sendQueue.enqueue(pendingGenChunk);
-+ }
-+ // try to move to tick queue
-+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
-+ this.tickingQueue.enqueue(pendingGenChunk);
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
+ }
-+ }
+
-+ // try to push more chunk generations
-+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
-+ final int maxGensThisTick = (int)this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, maxGens);
-+ int ratedGensThisTick = 0;
-+ while (!this.genQueue.isEmpty()) {
-+ final long chunkKey = this.genQueue.firstLong();
-+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
-+ final ChunkAccess chunk = this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ);
-+ if (chunk.getStatus() != ChunkStatus.FULL) {
-+ // only rate limit actual generations
-+ if ((ratedGensThisTick + 1) > maxGensThisTick) {
-+ break;
-+ }
-+ ++ratedGensThisTick;
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
+ }
+
-+ this.genQueue.dequeueLong();
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
+
-+ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING);
-+ if (prev != CHUNK_TICKET_STAGE_LOADED) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
+ }
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunkKey,
-+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
-+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ this.generatingQueue.enqueue(chunkKey);
-+ }
+
-+ // try to pull ticking chunks
-+ tick_check_outer:
-+ while (!this.tickingQueue.isEmpty()) {
-+ final long pendingTicking = this.tickingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
+
-+ final int tickingReq = 2;
-+ for (int dz = -tickingReq; dz <= tickingReq; ++dz) {
-+ for (int dx = -tickingReq; dx <= tickingReq; ++dx) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+ final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ);
-+ final byte stage = this.chunkTicketStage.get(neighbour);
-+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
-+ break tick_check_outer;
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
+ }
+ }
+ }
-+ // only gets here if all neighbours were marked as generated or ticking themselves
-+ this.tickingQueue.dequeueLong();
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addAndRemove(
-+ pendingTicking,
-+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
-+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ // there is no queue to add after ticking
-+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
-+ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
-+ }
+ }
++ }
+
-+ // try to pull sending chunks
-+ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // no logic to track concurrent sends
-+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
-+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
-+ for (int i = 0; i < maxSendsThisTick; ++i) {
-+ final long pendingSend = this.sendQueue.firstLong();
-+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
-+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
-+ final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ);
-+ if (!chunk.areNeighboursLoaded(1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
-+ // nothing to do
-+ // the target chunk may not be owned by this region, but this should be resolved in the future
-+ break;
-+ }
-+ if (!chunk.isPostProcessingDone) {
-+ // not yet post-processed, need to do this so that tile entities can properly be sent to clients
-+ chunk.postProcessGeneration();
-+ // check if there was any recursive action
-+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
-+ return;
-+ } // else: good to dequeue and send, fall through
-+ }
-+ this.sendQueue.dequeueLong();
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ab2fa1563d5e32a5313dfcc1da411cab45fb5ca0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+@@ -0,0 +1,718 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ this.sendChunk(pendingSendX, pendingSendZ);
-+ if (this.removed) {
-+ // sendChunk may invoke plugin logic
-+ return;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++
++public final class Delayed8WayDistancePropagator2D {
++
++ // Test
++ /*
++ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) {
++ int got = test.getLevel(x, z);
++
++ int expect = 0;
++ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet();
++ if (nearest != null) {
++ for (Object _obj : nearest) {
++ if (_obj instanceof Ticket) {
++ Ticket ticket = (Ticket)_obj;
++ long ticketCoord = reference.getLastCoordinate(ticket);
++ int viewDistance = reference.getLastViewDistance(ticket);
++ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x),
++ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z));
++ int level = viewDistance - distance;
++ if (level > expect) {
++ expect = level;
++ }
+ }
+ }
++ }
+
-+ this.flushDelayedTicketOps();
-+ // we assume propagate ticket levels happens after this call
++ if (expect != got) {
++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
+ }
++ }
+
-+ void add() {
-+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Adding removed player chunk loader");
-+ }
-+ final ViewDistances playerDistances = this.player.getViewDistances();
-+ final ViewDistances worldDistances = this.world.getViewDistances();
-+ final int chunkX = this.player.chunkPosition().x;
-+ final int chunkZ = this.player.chunkPosition().z;
++ static class Ticket {
+
-+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance);
-+ // load view cannot be less-than tick view + 1
-+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
-+ // send view cannot be greater-than load view
-+ final int clientViewDistance = getClientViewDistance(this.player);
-+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
++ int x;
++ int z;
+
-+ // send view distances
-+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
-+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty
++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
+
-+ // add to distance maps
-+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1);
-+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
-+ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
++ }
+
-+ // update chunk center
-+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
++ public static void main(final String[] args) {
++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
++ @Override
++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) {
++ return object.empty;
++ }
++ };
++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
+
-+ // now we can update
-+ this.update();
-+ }
++ final int maxDistance = 64;
++ // test origin
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ // test single source
++ reference.add(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, 0, 0, originDistance/2);
++ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
+
-+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
-+ return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ));
++ reference.remove(originTicket);
++ test.removeSource(0, 0); test.propagateUpdates();
+ }
+
-+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
-+ final BelowZeroRetrogen belowZeroRetrogen;
-+ // see PortalForcer#findPortalAround
-+ return chunkAccess != null && (
-+ chunkAccess.getStatus() == ChunkStatus.FULL ||
-+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))
-+ );
-+ }
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = (i & 1) == 1 ? -i : i;
++ a.z = (i & 1) == 1 ? -i : i;
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
+
-+ void update() {
-+ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Updating removed player chunk loader");
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
+ }
-+ final ViewDistances playerDistances = this.player.getViewDistances();
-+ final ViewDistances worldDistances = this.world.getViewDistances();
+
-+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance);
-+ // load view cannot be less-than tick view + 1
-+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
-+ // send view cannot be greater-than load view
-+ final int clientViewDistance = getClientViewDistance(this.player);
-+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
++ // test ticket level decrease
+
-+ final ChunkPos playerPos = this.player.chunkPosition();
-+ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
-+ final int currentChunkX = playerPos.x;
-+ final int currentChunkZ = playerPos.z;
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
+
-+ final int prevChunkX = this.lastChunkX;
-+ final int prevChunkZ = this.lastChunkZ;
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
+
-+ if (
-+ // has view distance stayed the same?
-+ sendViewDistance == this.lastSendDistance
-+ && loadViewDistance == this.lastLoadDistance
-+ && tickViewDistance == this.lastTickDistance
++ // test ticket level increase
+
-+ // has our chunk stayed the same?
-+ && prevChunkX == currentChunkX
-+ && prevChunkZ == currentChunkZ
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
+
-+ // can we still generate chunks?
-+ && this.canGenerateChunks == canGenerateChunks
-+ ) {
-+ // nothing we care about changed, so we're not re-calculating
-+ return;
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
+ }
+
-+ // update distance maps
-+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1);
-+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
-+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
-+ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) {
-+ throw new IllegalStateException();
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
+ }
++ test.propagateUpdates();
+
-+ // update VDs for client
-+ // this should be after the distance map updates, as they will send unload packets
-+ if (this.lastSentChunkRadius != sendViewDistance) {
-+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
+ }
-+ if (this.lastSentSimulationDistance != tickViewDistance) {
-+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
++ }
++
++ // now test at coordinate offsets
++ // test offset
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ // test single source
++ reference.add(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, offX, offZ, originDistance/2);
++ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
+ }
+
-+ this.sendQueue.clear();
-+ this.tickingQueue.clear();
-+ this.generatingQueue.clear();
-+ this.genQueue.clear();
-+ this.loadingQueue.clear();
-+ this.loadQueue.clear();
++ reference.remove(originTicket);
++ test.removeSource(offX, offZ); test.propagateUpdates();
++ }
+
-+ this.lastChunkX = currentChunkX;
-+ this.lastChunkZ = currentChunkZ;
-+ this.lastSendDistance = sendViewDistance;
-+ this.lastLoadDistance = loadViewDistance;
-+ this.lastTickDistance = tickViewDistance;
-+ this.canGenerateChunks = canGenerateChunks;
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = offX + ((i & 1) == 1 ? -i : i);
++ a.z = offZ + ((i & 1) == 1 ? -i : i);
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
+
-+ // +1 since we need to load chunks +1 around the load view distance...
-+ final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1];
-+ // the iteration order is by increasing manhattan distance - so, we do NOT need to
-+ // sort anything in the queue!
-+ for (final long deltaChunk : toIterate) {
-+ final int dx = CoordinateUtils.getChunkX(deltaChunk);
-+ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
-+ final int chunkX = dx + currentChunkX;
-+ final int chunkZ = dz + currentChunkZ;
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
-+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
+
-+ // since chunk sending is not by radius alone, we need an extra check here to account for
-+ // everything <= sendDistance
-+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
-+ // the dist <= view check
-+ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1))
-+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
-+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
++ // test ticket level decrease
+
-+ if (!sendChunk && sentChunk) {
-+ // have sent the chunk, but don't want it anymore
-+ // unload it now
-+ this.sendUnloadChunkRaw(chunkX, chunkZ);
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
+ }
++ }
+
-+ final byte stage = this.chunkTicketStage.get(chunk);
-+ switch (stage) {
-+ case CHUNK_TICKET_STAGE_NONE: {
-+ // we want the chunk to be at least loaded
-+ this.loadQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_LOADING: {
-+ this.loadingQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_LOADED: {
-+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
-+ this.genQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_GENERATING: {
-+ this.generatingQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_GENERATED: {
-+ if (sendChunk && !sentChunk) {
-+ this.sendQueue.enqueue(chunk);
-+ }
-+ if (squareDistance <= tickViewDistance) {
-+ this.tickingQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_TICK: {
-+ if (sendChunk && !sentChunk) {
-+ this.sendQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown stage: " + stage);
-+ }
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
+ }
+ }
+
-+ // update the chunk center
-+ // this must be done last so that the client does not ignore any of our unload chunk packets above
-+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
-+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
++ continue;
++ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
+ }
++ test.propagateUpdates();
+
-+ this.flushDelayedTicketOps();
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
+ }
++ }
++ */
+
-+ void remove() {
-+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Removing removed player chunk loader");
-+ }
-+ this.removed = true;
-+ // sends the chunk unload packets
-+ this.broadcastMap.remove();
-+ // cleans up loading/generating tickets
-+ this.loadTicketCleanup.remove();
-+ // cleans up ticking tickets
-+ this.tickMap.remove();
++ // this map is considered "stale" unless updates are propagated.
++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
+
-+ // purge queues
-+ this.sendQueue.clear();
-+ this.tickingQueue.clear();
-+ this.generatingQueue.clear();
-+ this.genQueue.clear();
-+ this.loadingQueue.clear();
-+ this.loadQueue.clear();
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
+
-+ // flush ticket changes
-+ this.flushDelayedTicketOps();
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
+
-+ // now all tickets should be removed, which is all of our external state
-+ }
+ }
+
-+ // TODO rebase into util patch
-+ private static final class AllocatingRateLimiter {
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed8WayDistancePropagator2D() {
++ this(null);
++ }
+
-+ // max difference granularity in ns
-+ private static final long MAX_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
+
-+ private double allocation;
-+ private long lastAllocationUpdate;
-+ private double takeCarry;
-+ private long lastTakeUpdate;
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkKey(x, z));
++ }
+
-+ // rate in units/s, and time in ns
-+ public void tickAllocation(final long time, final double rate, final double maxAllocation) {
-+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastAllocationUpdate);
-+ this.lastAllocationUpdate = time;
++ public void setSource(final int x, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkKey(x, z), level);
++ }
+
-+ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
+ }
+
-+ // rate in units/s, and time in ns
-+ public long takeAllocation(final long time, final double rate, final long maxTake) {
-+ if (maxTake < 1L) {
-+ return 0L;
-+ }
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
+
-+ double ret = this.takeCarry;
-+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastTakeUpdate);
-+ this.lastTakeUpdate = time;
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
+
-+ // note: abs(takeCarry) <= 1.0
-+ final double take = Math.min(Math.min((double)maxTake - this.takeCarry, this.allocation), rate * (diff*1.0E-9));
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
+
-+ ret += take;
-+ this.allocation -= take;
++ public void removeSource(final int x, final int z) {
++ this.removeSource(CoordinateUtils.getChunkKey(x, z));
++ }
+
-+ final long retInteger = (long)Math.floor(ret);
-+ this.takeCarry = ret - (double)retInteger;
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
+
-+ return retInteger;
++ // queues used for BFS propagating levels
++ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new WorkQueue();
+ }
+ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
+
-+ static final class CountedSRSWLinkedQueue<E> {
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ private final SRSWLinkedQueue<E> queue = new SRSWLinkedQueue<>();
-+ private volatile long countAdded;
-+ private volatile long countRemoved;
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
+
-+ private static final VarHandle COUNT_ADDED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countAdded", long.class);
-+ private static final VarHandle COUNT_REMOVED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countRemoved", long.class);
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ private long getCountAddedPlain() {
-+ return (long)COUNT_ADDED_HANDLE.get(this);
-+ }
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
+
-+ private long getCountAddedAcquire() {
-+ return (long)COUNT_ADDED_HANDLE.getAcquire(this);
-+ }
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ private void setCountAddedRelease(final long to) {
-+ COUNT_ADDED_HANDLE.setRelease(this, to);
-+ }
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
+
-+ private long getCountRemovedPlain() {
-+ return (long)COUNT_REMOVED_HANDLE.get(this);
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
+ }
+
-+ private long getCountRemovedAcquire() {
-+ return (long)COUNT_REMOVED_HANDLE.getAcquire(this);
-+ }
++ boolean ret = false;
+
-+ private void setCountRemovedRelease(final long to) {
-+ COUNT_REMOVED_HANDLE.setRelease(this, to);
-+ }
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
+
-+ public void add(final E element) {
-+ this.setCountAddedRelease(this.getCountAddedPlain() + 1L);
-+ this.queue.addLast(element);
-+ }
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
+
-+ public E poll() {
-+ final E ret = this.queue.poll();
-+ if (ret != null) {
-+ this.setCountRemovedRelease(this.getCountRemovedPlain() + 1L);
++ if (currentLevel == updatedSource) {
++ continue;
+ }
++ ret = true;
+
-+ return ret;
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
+ }
+
-+ public long size() {
-+ final long removed = this.getCountRemovedAcquire();
-+ final long added = this.getCountAddedAcquire();
++ this.updatedSources.clear();
+
-+ return added - removed;
-+ }
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
+ }
+
-+ private static class CustomLongArray extends LongArrayList {
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
+
-+ public CustomLongArray() {
-+ super();
-+ }
++ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
+
-+ public CustomLongArray(final int expected) {
-+ super(expected);
-+ }
++ final boolean neighbourCheck = level < 0;
+
-+ public boolean addAll(final CustomLongArray list) {
-+ this.addElements(this.size, list.a, 0, list.size);
-+ return list.size != 0;
-+ }
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
+
-+ public void addUnchecked(final long value) {
-+ this.a[this.size++] = value;
-+ }
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
+
-+ public void forceSize(final int to) {
-+ this.size = to;
-+ }
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
+
-+ @Override
-+ public int hashCode() {
-+ long h = 1L;
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
+
-+ Objects.checkFromToIndex(0, this.size, this.a.length);
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
+
-+ for (int i = 0; i < this.size; ++i) {
-+ h = it.unimi.dsi.fastutil.HashCommon.mix(h + this.a[i]);
-+ }
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
+
-+ return (int)h;
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
+ }
++ }
+
-+ @Override
-+ public boolean equals(final Object o) {
-+ if (o == this) {
-+ return true;
-+ }
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
+
-+ if (!(o instanceof CustomLongArray other)) {
-+ return false;
-+ }
++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
+
-+ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size);
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
+ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
+ }
+
-+ private static int getDistanceSize(final int radius, final int max) {
-+ if (radius == 0) {
-+ return 1;
-+ }
-+ final int diff = radius - max;
-+ if (diff <= 0) {
-+ return 4*radius;
++ protected static final class LevelMap extends Long2ByteOpenHashMap {
++ public LevelMap() {
++ super();
+ }
-+ return 4*(max - Math.max(0, diff - 1));
-+ }
+
-+ private static int getQ1DistanceSize(final int radius, final int max) {
-+ if (radius == 0) {
-+ return 1;
++ public LevelMap(final int expected, final float loadFactor) {
++ super(expected, loadFactor);
+ }
-+ final int diff = radius - max;
-+ if (diff <= 0) {
-+ return radius+1;
++
++ // copied from superclass
++ private int find(final long k) {
++ if (k == 0L) {
++ return this.containsNullKey ? this.n : -(this.n + 1);
++ } else {
++ final long[] key = this.key;
++ long curr;
++ int pos;
++ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
++ return -(pos + 1);
++ } else if (k == curr) {
++ return pos;
++ } else {
++ while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
++ if (k == curr) {
++ return pos;
++ }
++ }
++
++ return -(pos + 1);
++ }
++ }
+ }
-+ return max - diff + 1;
-+ }
+
-+ private static final class BasicFIFOLQueue {
++ // copied from superclass
++ private void insert(final int pos, final long k, final byte v) {
++ if (pos == this.n) {
++ this.containsNullKey = true;
++ }
+
-+ private final long[] values;
-+ private int head, tail;
++ this.key[pos] = k;
++ this.value[pos] = v;
++ if (this.size++ >= this.maxFill) {
++ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++ }
++ }
+
-+ public BasicFIFOLQueue(final int cap) {
-+ if (cap <= 1) {
-+ throw new IllegalArgumentException();
++ // copied from superclass
++ public byte putIfGreater(final long key, final byte value) {
++ final int pos = this.find(key);
++ if (pos < 0) {
++ if (this.defRetValue < value) {
++ this.insert(-pos - 1, key, value);
++ }
++ return this.defRetValue;
++ } else {
++ final byte curr = this.value[pos];
++ if (value > curr) {
++ this.value[pos] = value;
++ return curr;
++ }
++ return curr;
+ }
-+ this.values = new long[cap];
+ }
+
-+ public boolean isEmpty() {
-+ return this.head == this.tail;
++ // copied from superclass
++ private void removeEntry(final int pos) {
++ --this.size;
++ this.shiftKeys(pos);
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
+ }
+
-+ public long removeFirst() {
-+ final long ret = this.values[this.head];
++ // copied from superclass
++ private void removeNullEntry() {
++ this.containsNullKey = false;
++ --this.size;
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
+
-+ if (this.head == this.tail) {
-+ throw new IllegalStateException();
++ // copied from superclass
++ public byte removeIfGreaterOrEqual(final long key, final byte value) {
++ if (key == 0L) {
++ if (!this.containsNullKey) {
++ return this.defRetValue;
++ }
++ final byte current = this.value[this.n];
++ if (value >= current) {
++ this.removeNullEntry();
++ return current;
++ }
++ return current;
++ } else {
++ long[] keys = this.key;
++ byte[] values = this.value;
++ long curr;
++ int pos;
++ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
++ return this.defRetValue;
++ } else if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ } else {
++ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
++ if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ }
++ }
++
++ return this.defRetValue;
++ }
+ }
++ }
++ }
+
-+ ++this.head;
-+ if (this.head == this.values.length) {
-+ this.head = 0;
++ protected static final class WorkQueue {
++
++ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
++ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
++
++ }
++
++ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public long removeFirstLong() {
++ // copied from superclass
++ long t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
+ }
+
-+ return ret;
++ return t;
+ }
++ }
+
-+ public void addLast(final long value) {
-+ this.values[this.tail++] = value;
++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
+
-+ if (this.tail == this.head) {
-+ throw new IllegalStateException();
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public byte removeFirstByte() {
++ // copied from superclass
++ byte t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
+ }
+
-+ if (this.tail == this.values.length) {
-+ this.tail = 0;
-+ }
++ return t;
+ }
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..61f70247486fd15ed3ffc5b606582dc6a2dd81d3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+@@ -0,0 +1,232 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ private static CustomLongArray[] makeQ1BFS(final int radius) {
-+ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1];
-+ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1);
-+ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1));
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+
-+ seen.add(CoordinateUtils.getChunkKey(0, 0));
-+ queue.addLast(CoordinateUtils.getChunkKey(0, 0));
-+ while (!queue.isEmpty()) {
-+ final long chunk = queue.removeFirst();
-+ final int chunkX = CoordinateUtils.getChunkX(chunk);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
++public abstract class SingleUserAreaMap<T> {
+
-+ final int index = Math.abs(chunkX) + Math.abs(chunkZ);
-+ final CustomLongArray list = ret[index];
-+ if (list != null) {
-+ list.addUnchecked(chunk);
-+ } else {
-+ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk);
-+ }
++ private static final int NOT_SET = Integer.MIN_VALUE;
+
-+ for (int i = 0; i < 4; ++i) {
-+ // 0 -> -1, 0
-+ // 1 -> 0, -1
-+ // 2 -> 1, 0
-+ // 3 -> 0, 1
++ private final T parameter;
++ private int lastChunkX = NOT_SET;
++ private int lastChunkZ = NOT_SET;
++ private int distance = NOT_SET;
+
-+ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0)
-+ // note: -n = (~n) + 1
-+ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1)
++ public SingleUserAreaMap(final T parameter) {
++ this.parameter = parameter;
++ }
+
-+ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1
-+ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0
-+ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1
++ /* math sign function except 0 returns 1 */
++ protected static int sign(int val) {
++ return 1 | (val >> (Integer.SIZE - 1));
++ }
+
-+ final int neighbourX = chunkX + dx;
-+ final int neighbourZ = chunkZ + dz;
-+ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
+
-+ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
-+ // don't enqueue out of range
-+ continue;
-+ }
++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
+
-+ if (!seen.add(neighbour)) {
-+ continue;
-+ }
++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
+
-+ queue.addLast(neighbour);
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.addCallback(parameter, cx, cz);
+ }
+ }
-+
-+ return ret;
+ }
+
-+ // doesn't appear worth optimising this function now, even though it's 70% of the call
-+ private static CustomLongArray spread(final CustomLongArray input, final int size) {
-+ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input);
-+ final CustomLongArray added = new CustomLongArray(size);
++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
+
-+ while (!notAdded.isEmpty()) {
-+ if (added.isEmpty()) {
-+ added.addUnchecked(notAdded.removeLastLong());
-+ continue;
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.removeCallback(parameter, cx, cz);
+ }
++ }
++ }
+
-+ long maxChunk = -1L;
-+ int maxDist = 0;
++ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
++ if (distance < 0) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ if (this.lastChunkX != NOT_SET) {
++ return false;
++ }
++ this.lastChunkX = chunkX;
++ this.lastChunkZ = chunkZ;
++ this.distance = distance;
+
-+ // select the chunk from the not yet added set that has the largest minimum distance from
-+ // the current set of added chunks
++ this.addToNew(this.parameter, chunkX, chunkZ, distance);
+
-+ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) {
-+ final long chunkKey = iterator.nextLong();
-+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
++ return true;
++ }
+
-+ int minDist = Integer.MAX_VALUE;
++ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
++ if (newViewDistance < 0) {
++ throw new IllegalArgumentException(Integer.toString(newViewDistance));
++ }
++ final int fromX = this.lastChunkX;
++ final int fromZ = this.lastChunkZ;
++ final int oldViewDistance = this.distance;
++ if (fromX == NOT_SET) {
++ return false;
++ }
+
-+ final int len = added.size();
-+ final long[] addedArr = added.elements();
-+ Objects.checkFromToIndex(0, len, addedArr.length);
-+ for (int i = 0; i < len; ++i) {
-+ final long addedKey = addedArr[i];
-+ final int addedX = CoordinateUtils.getChunkX(addedKey);
-+ final int addedZ = CoordinateUtils.getChunkZ(addedKey);
++ this.lastChunkX = toX;
++ this.lastChunkZ = toZ;
++ this.distance = newViewDistance;
+
-+ // here we use square distance because chunk generation uses neighbours in a square radius
-+ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ));
++ final T parameter = this.parameter;
+
-+ minDist = Math.min(dist, minDist);
-+ }
+
-+ if (minDist > maxDist) {
-+ maxDist = minDist;
-+ maxChunk = chunkKey;
++ final int dx = toX - fromX;
++ final int dz = toZ - fromZ;
++
++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
++
++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
++ // teleported
++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
++ this.addToNew(parameter, toX, toZ, newViewDistance);
++ return true;
++ }
++
++ if (oldViewDistance != newViewDistance) {
++ // remove loop
++
++ final int oldMinX = fromX - oldViewDistance;
++ final int oldMinZ = fromZ - oldViewDistance;
++ final int oldMaxX = fromX + oldViewDistance;
++ final int oldMaxZ = fromZ + oldViewDistance;
++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
++
++ // only remove if we're outside the new view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
++ this.removeCallback(parameter, currX, currZ);
++ }
+ }
+ }
+
-+ // move the selected chunk from the not added set to the added set
++ // add loop
+
-+ if (!notAdded.remove(maxChunk)) {
-+ throw new IllegalStateException();
++ final int newMinX = toX - newViewDistance;
++ final int newMinZ = toZ - newViewDistance;
++ final int newMaxX = toX + newViewDistance;
++ final int newMaxZ = toZ + newViewDistance;
++ for (int currX = newMinX; currX <= newMaxX; ++currX) {
++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
++
++ // only add if we're outside the old view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
+ }
+
-+ added.addUnchecked(maxChunk);
++ return true;
+ }
+
-+ return added;
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..15ee41452992714108efe53b708b5a4e1da7c1ff
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
-@@ -0,0 +1,902 @@
-+package io.papermc.paper.chunk.system.entity;
++ // x axis is width
++ // z axis is height
++ // right refers to the x axis of where we moved
++ // top refers to the z axis of where we moved
+
-+import com.destroystokyo.paper.util.maplist.EntityList;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import io.papermc.paper.util.WorldUtil;
-+import io.papermc.paper.world.ChunkEntitySlices;
-+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap;
-+import net.minecraft.core.BlockPos;
-+import io.papermc.paper.chunk.system.ChunkSystem;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.AbortableIterationConsumer;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.EntityType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.entity.EntityInLevelCallback;
-+import net.minecraft.world.level.entity.EntityTypeTest;
-+import net.minecraft.world.level.entity.LevelCallback;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.world.level.entity.LevelEntityGetter;
-+import net.minecraft.world.level.entity.Visibility;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
-+import org.jetbrains.annotations.NotNull;
-+import org.jetbrains.annotations.Nullable;
-+import org.slf4j.Logger;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.NoSuchElementException;
-+import java.util.UUID;
-+import java.util.concurrent.locks.StampedLock;
-+import java.util.function.Consumer;
-+import java.util.function.Predicate;
++ // same view distance
+
-+public final class EntityLookup implements LevelEntityGetter<Entity> {
++ // used for relative positioning
++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ // The area excluded by overlapping the two view distance squares creates four rectangles:
++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
++ // and on the right the "added" section.
++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
++ // exclusive to the regions they surround.
+
-+ protected static final int REGION_SHIFT = 5;
-+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
-+ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
++ // 4 points of the rectangle
++ int maxX; // exclusive
++ int minX; // inclusive
++ int maxZ; // exclusive
++ int minZ; // inclusive
+
-+ public final ServerLevel world;
++ if (dx != 0) {
++ // handle right addition
+
-+ private final StampedLock stateLock = new StampedLock();
-+ protected final Long2ObjectOpenHashMap<ChunkSlicesRegion> regions = new Long2ObjectOpenHashMap<>(128, 0.5f);
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX + (oldViewDistance * right) + right; // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
+
-+ private final int minSection; // inclusive
-+ private final int maxSection; // inclusive
-+ private final LevelCallback<Entity> worldCallback;
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
+
-+ private final StampedLock entityByLock = new StampedLock();
-+ private final Int2ReferenceOpenHashMap<Entity> entityById = new Int2ReferenceOpenHashMap<>();
-+ private final Object2ReferenceOpenHashMap<UUID, Entity> entityByUUID = new Object2ReferenceOpenHashMap<>();
-+ private final EntityList accessibleEntities = new EntityList();
++ if (dz != 0) {
++ // handle up addition
+
-+ public EntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
-+ this.world = world;
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+ this.worldCallback = worldCallback;
-+ }
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = toX - (oldViewDistance * right); // inclusive
++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
-+ private static Entity maskNonAccessible(final Entity entity) {
-+ if (entity == null) {
-+ return null;
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
+ }
-+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
-+ return visibility.isAccessible() ? entity : null;
-+ }
+
-+ @Nullable
-+ @Override
-+ public Entity get(final int id) {
-+ final long attempt = this.entityByLock.tryOptimisticRead();
-+ if (attempt != 0L) {
-+ try {
-+ final Entity ret = this.entityById.get(id);
++ if (dx != 0) {
++ // handle left removal
++
++ maxX = toX - (oldViewDistance * right); // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
+
-+ if (this.entityByLock.validate(attempt)) {
-+ return maskNonAccessible(ret);
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
+ }
-+ } catch (final Error error) {
-+ throw error;
-+ } catch (final Throwable thr) {
-+ // ignore
+ }
+ }
+
-+ this.entityByLock.readLock();
-+ try {
-+ return maskNonAccessible(this.entityById.get(id));
-+ } finally {
-+ this.entityByLock.tryUnlockRead();
-+ }
-+ }
++ if (dz != 0) {
++ // handle down removal
+
-+ @Nullable
-+ @Override
-+ public Entity get(final UUID id) {
-+ final long attempt = this.entityByLock.tryOptimisticRead();
-+ if (attempt != 0L) {
-+ try {
-+ final Entity ret = this.entityByUUID.get(id);
++ maxX = fromX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = toZ - (oldViewDistance * up); // exclusive
++ minZ = fromZ - (oldViewDistance * up); // inclusive
+
-+ if (this.entityByLock.validate(attempt)) {
-+ return maskNonAccessible(ret);
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
+ }
-+ } catch (final Error error) {
-+ throw error;
-+ } catch (final Throwable thr) {
-+ // ignore
+ }
+ }
+
-+ this.entityByLock.readLock();
-+ try {
-+ return maskNonAccessible(this.entityByUUID.get(id));
-+ } finally {
-+ this.entityByLock.tryUnlockRead();
-+ }
++ return true;
+ }
+
-+ public boolean hasEntity(final UUID uuid) {
-+ return this.get(uuid) != null;
-+ }
++ public final boolean remove() {
++ final int chunkX = this.lastChunkX;
++ final int chunkZ = this.lastChunkZ;
++ final int distance = this.distance;
++ if (chunkX == NOT_SET) {
++ return false;
++ }
+
-+ public String getDebugInfo() {
-+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size();
++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
++
++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
++
++ return true;
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
+@@ -0,0 +1,68 @@
++package ca.spottedleaf.moonrise.common.set;
+
-+ static final class ArrayIterable<T> implements Iterable<T> {
++import java.util.Collection;
+
-+ private final T[] array;
-+ private final int off;
-+ private final int length;
++public final class OptimizedSmallEnumSet<E extends Enum<E>> {
+
-+ public ArrayIterable(final T[] array, final int off, final int length) {
-+ this.array = array;
-+ this.off = off;
-+ this.length = length;
-+ if (length > array.length) {
-+ throw new IllegalArgumentException("Length must be no greater-than the array length");
-+ }
-+ }
++ private final Class<E> enumClass;
++ private long backingSet;
+
-+ @NotNull
-+ @Override
-+ public Iterator<T> iterator() {
-+ return new ArrayIterator<>(this.array, this.off, this.length);
++ public OptimizedSmallEnumSet(final Class<E> clazz) {
++ if (clazz == null) {
++ throw new IllegalArgumentException("Null class");
++ }
++ if (!clazz.isEnum()) {
++ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
+ }
++ this.enumClass = clazz;
++ }
+
-+ static final class ArrayIterator<T> implements Iterator<T> {
++ public boolean addUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
+
-+ private final T[] array;
-+ private int off;
-+ private final int length;
++ final long prev = this.backingSet;
++ this.backingSet = prev | key;
+
-+ public ArrayIterator(final T[] array, final int off, final int length) {
-+ this.array = array;
-+ this.off = off;
-+ this.length = length;
-+ }
++ return (prev & key) == 0;
++ }
+
-+ @Override
-+ public boolean hasNext() {
-+ return this.off < this.length;
-+ }
++ public boolean removeUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
+
-+ @Override
-+ public T next() {
-+ if (this.off >= this.length) {
-+ throw new NoSuchElementException();
-+ }
-+ return this.array[this.off++];
-+ }
++ final long prev = this.backingSet;
++ this.backingSet = prev & ~key;
+
-+ @Override
-+ public void remove() {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
++ return (prev & key) != 0;
+ }
+
-+ @Override
-+ public Iterable<Entity> getAll() {
-+ return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size());
++ public void clear() {
++ this.backingSet = 0L;
+ }
+
-+ public Entity[] getAllCopy() {
-+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
++ public int size() {
++ return Long.bitCount(this.backingSet);
+ }
+
-+ @Override
-+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) {
-+ final Int2ReferenceOpenHashMap<Entity> entityCopy;
-+
-+ this.entityByLock.readLock();
-+ try {
-+ entityCopy = this.entityById.clone();
-+ } finally {
-+ this.entityByLock.tryUnlockRead();
-+ }
-+ for (final Entity entity : entityCopy.values()) {
-+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
-+ if (!visibility.isAccessible()) {
-+ continue;
-+ }
-+ final U casted = filter.tryCast(entity);
-+ if (casted != null && action.accept(casted).shouldAbort()) {
-+ break;
++ public void addAllUnchecked(final Collection<E> enums) {
++ for (final E element : enums) {
++ if (element == null) {
++ throw new NullPointerException("Null element");
+ }
++ this.backingSet |= (1L << element.ordinal());
+ }
+ }
+
-+ @Override
-+ public void get(final AABB box, final Consumer<Entity> action) {
-+ List<Entity> entities = new ArrayList<>();
-+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ action.accept(entities.get(i));
-+ }
++ public long getBackingSet() {
++ return this.backingSet;
+ }
+
-+ @Override
-+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) {
-+ List<Entity> entities = new ArrayList<>();
-+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final U casted = filter.tryCast(entities.get(i));
-+ if (casted != null && action.accept(casted).shouldAbort()) {
-+ break;
-+ }
-+ }
++ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
++ return (other.backingSet & this.backingSet) != 0;
+ }
+
-+ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved,
-+ final boolean created, final boolean destroyed) {
-+ TickThread.ensureTickThread(entity, "Entity status change must only happen on the main thread");
++ public boolean hasElement(final E element) {
++ return (this.backingSet & (1L << element.ordinal())) != 0;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..31b92bd48828cbea25b44a9f0f96886347aa1ae6
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.util;
+
-+ if (entity.updatingSectionStatus) {
-+ // recursive status update
-+ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable());
-+ return;
-+ }
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.SectionPos;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.Vec3;
+
-+ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
++public final class CoordinateUtils {
+
-+ if (entityStatusUpdateBefore) {
-+ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
-+ return;
-+ }
++ // the chunk keys are compatible with vanilla
+
-+ try {
-+ final Boolean ticketBlockBefore = this.world.chunkTaskScheduler.chunkHolderManager.blockTicketUpdates();
-+ try {
-+ entity.updatingSectionStatus = true;
-+ try {
-+ if (created) {
-+ EntityLookup.this.worldCallback.onCreated(entity);
-+ }
++ public static long getChunkKey(final BlockPos pos) {
++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
++ }
+
-+ if (oldVisibility == newVisibility) {
-+ if (moved && newVisibility.isAccessible()) {
-+ EntityLookup.this.worldCallback.onSectionChange(entity);
-+ }
-+ return;
-+ }
++ public static long getChunkKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
++ }
+
-+ if (newVisibility.ordinal() > oldVisibility.ordinal()) {
-+ // status upgrade
-+ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
-+ this.accessibleEntities.add(entity);
-+ EntityLookup.this.worldCallback.onTrackingStart(entity);
-+ }
++ public static long getChunkKey(final ChunkPos pos) {
++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
++ }
+
-+ if (!oldVisibility.isTicking() && newVisibility.isTicking()) {
-+ EntityLookup.this.worldCallback.onTickingStart(entity);
-+ }
-+ } else {
-+ // status downgrade
-+ if (oldVisibility.isTicking() && !newVisibility.isTicking()) {
-+ EntityLookup.this.worldCallback.onTickingEnd(entity);
-+ }
++ public static long getChunkKey(final SectionPos pos) {
++ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
++ }
+
-+ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
-+ this.accessibleEntities.remove(entity);
-+ EntityLookup.this.worldCallback.onTrackingEnd(entity);
-+ }
-+ }
++ public static long getChunkKey(final int x, final int z) {
++ return ((long)z << 32) | (x & 0xFFFFFFFFL);
++ }
+
-+ if (moved && newVisibility.isAccessible()) {
-+ EntityLookup.this.worldCallback.onSectionChange(entity);
-+ }
++ public static int getChunkX(final long chunkKey) {
++ return (int)chunkKey;
++ }
+
-+ if (destroyed) {
-+ EntityLookup.this.worldCallback.onDestroyed(entity);
-+ }
-+ } finally {
-+ entity.updatingSectionStatus = false;
-+ }
-+ } finally {
-+ this.world.chunkTaskScheduler.chunkHolderManager.unblockTicketUpdates(ticketBlockBefore);
-+ }
-+ } finally {
-+ if (slices != null) {
-+ slices.stopPreventingStatusUpdates(false);
-+ }
-+ }
++ public static int getChunkZ(final long chunkKey) {
++ return (int)(chunkKey >>> 32);
+ }
+
-+ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
-+ this.getChunk(x, z).updateStatus(newStatus, this);
++ public static int getChunkCoordinate(final double blockCoordinate) {
++ return Mth.floor(blockCoordinate) >> 4;
+ }
+
-+ public void addLegacyChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, true);
++ // the section keys are compatible with vanilla's
++
++ static final int SECTION_X_BITS = 22;
++ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
++ static final int SECTION_Y_BITS = 20;
++ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
++ static final int SECTION_Z_BITS = 22;
++ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
++ // format is y,z,x (in order of LSB to MSB)
++ static final int SECTION_Y_SHIFT = 0;
++ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
++ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
++ static final int SECTION_TO_BLOCK_SHIFT = 4;
++
++ public static long getChunkSectionKey(final int x, final int y, final int z) {
++ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
-+ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, true);
++ public static long getChunkSectionKey(final SectionPos pos) {
++ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
-+ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, false);
++ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
++ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
++ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
-+ private void addRecursivelySafe(final Entity root, final boolean fromDisk) {
-+ if (!this.addEntity(root, fromDisk)) {
-+ // possible we are a passenger, and so should dismount from any valid entity in the world
-+ root.stopRiding(true);
-+ return;
-+ }
-+ for (final Entity passenger : root.getPassengers()) {
-+ this.addRecursivelySafe(passenger, fromDisk);
-+ }
++ public static long getChunkSectionKey(final BlockPos pos) {
++ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
+ }
+
-+ private void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) {
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity entity = entities.get(i);
-+ if (entity.isPassenger()) {
-+ continue;
-+ }
++ public static long getChunkSectionKey(final Entity entity) {
++ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
++ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
++ }
+
-+ if (!entity.chunkPosition().equals(forChunk)) {
-+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk);
-+ // can't set removed here, as we may not own the chunk position
-+ // skip the entity
-+ continue;
-+ }
++ public static int getChunkSectionX(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
++ }
+
-+ final Vec3 rootPosition = entity.position();
++ public static int getChunkSectionY(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
++ }
+
-+ // always adjust positions before adding passengers in case plugins access the entity, and so that
-+ // they are added to the right entity chunk
-+ for (final Entity passenger : entity.getIndirectPassengers()) {
-+ if (!passenger.chunkPosition().equals(forChunk)) {
-+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z, true);
-+ }
-+ }
++ public static int getChunkSectionZ(final long key) {
++ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
++ }
+
-+ this.addRecursivelySafe(entity, fromDisk);
-+ }
++ public static int getBlockX(final Vec3 pos) {
++ return Mth.floor(pos.x);
+ }
+
-+ public boolean addNewEntity(final Entity entity) {
-+ return this.addEntity(entity, false);
++ public static int getBlockY(final Vec3 pos) {
++ return Mth.floor(pos.y);
+ }
+
-+ public static Visibility getEntityStatus(final Entity entity) {
-+ if (entity.isAlwaysTicking()) {
-+ return Visibility.TICKING;
-+ }
-+ final FullChunkStatus entityStatus = entity.chunkStatus;
-+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
++ public static int getBlockZ(final Vec3 pos) {
++ return Mth.floor(pos.z);
+ }
+
-+ private boolean addEntity(final Entity entity, final boolean fromDisk) {
-+ final BlockPos pos = entity.blockPosition();
-+ final int sectionX = pos.getX() >> 4;
-+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
-+ final int sectionZ = pos.getZ() >> 4;
-+ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot add entity off-main thread");
++ public static int getChunkX(final Vec3 pos) {
++ return Mth.floor(pos.x) >> 4;
++ }
+
-+ if (entity.isRemoved()) {
-+ LOGGER.warn("Refusing to add removed entity: " + entity);
-+ return false;
-+ }
++ public static int getChunkY(final Vec3 pos) {
++ return Mth.floor(pos.y) >> 4;
++ }
+
-+ if (entity.updatingSectionStatus) {
-+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable());
-+ return false;
-+ }
++ public static int getChunkZ(final Vec3 pos) {
++ return Mth.floor(pos.z) >> 4;
++ }
+
-+ if (fromDisk) {
-+ ChunkSystem.onEntityPreAdd(this.world, entity);
-+ if (entity.isRemoved()) {
-+ // removed from checkDupeUUID call
-+ return false;
-+ }
++ private CoordinateUtils() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0531f25aaad162386a029d33e68d7c8336b9d5d1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
+@@ -0,0 +1,109 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import java.util.Objects;
++
++public final class FlatBitsetUtil {
++
++ private static final int LOG2_LONG = 6;
++ private static final long ALL_SET = -1L;
++ private static final int BITS_PER_LONG = Long.SIZE;
++
++ // from inclusive
++ // to exclusive
++ public static int firstSet(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
+ }
+
-+ this.entityByLock.writeLock();
-+ try {
-+ if (this.entityById.containsKey(entity.getId())) {
-+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + this.entityById.get(entity.getId()) + ", can't add " + entity);
-+ return false;
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
+ }
-+ if (this.entityByUUID.containsKey(entity.getUUID())) {
-+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + this.entityByUUID.get(entity.getUUID()) + ", can't add " + entity);
-+ return false;
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
+ }
-+ this.entityById.put(entity.getId(), entity);
-+ this.entityByUUID.put(entity.getUUID(), entity);
-+ } finally {
-+ this.entityByLock.tryUnlockWrite();
++
++ tmp = bitset[++bitsetIdx];
+ }
++ }
+
-+ entity.sectionX = sectionX;
-+ entity.sectionY = sectionY;
-+ entity.sectionZ = sectionZ;
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
-+ if (!slices.addEntity(entity, sectionY)) {
-+ LOGGER.warn("Entity " + entity + " added to world '" + this.world.getWorld().getName() + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")");
++ // from inclusive
++ // to exclusive
++ public static int firstClear(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
+ }
++ // like firstSet, but invert the bitset
+
-+ entity.setLevelCallback(new EntityCallback(entity));
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
+
-+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
++ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
++ }
+
-+ return true;
-+ }
++ bitIdx += BITS_PER_LONG;
+
-+ public boolean canRemoveEntity(final Entity entity) {
-+ if (entity.updatingSectionStatus) {
-+ return false;
-+ }
++ if (bitIdx >= to) {
++ return -1;
++ }
+
-+ final int sectionX = entity.sectionX;
-+ final int sectionZ = entity.sectionZ;
-+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
-+ return slices == null || !slices.isPreventingStatusUpdates();
++ tmp = ~bitset[++bitsetIdx];
++ }
+ }
+
-+ private void removeEntity(final Entity entity) {
-+ final int sectionX = entity.sectionX;
-+ final int sectionY = entity.sectionY;
-+ final int sectionZ = entity.sectionZ;
-+ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot remove entity off-main");
-+ if (!entity.isRemoved()) {
-+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity");
++ // from inclusive
++ // to exclusive
++ public static void clearRange(final long[] bitset, final int from, int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
+ }
-+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
-+ // all entities should be in a chunk
-+ if (slices == null) {
-+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")");
-+ } else {
-+ if (slices.isPreventingStatusUpdates()) {
-+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates");
-+ }
-+ if (!slices.removeEntity(entity, sectionY)) {
-+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
-+ }
++
++ if (from == to) {
++ return;
+ }
-+ entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE;
+
-+ this.entityByLock.writeLock();
-+ try {
-+ if (!this.entityById.remove(entity.getId(), entity)) {
-+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + this.entityById.get(entity.getId()));
-+ }
-+ if (!this.entityByUUID.remove(entity.getUUID(), entity)) {
-+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + this.entityByUUID.get(entity.getUUID()));
++ --to;
++
++ final int fromBitsetIdx = from >>> LOG2_LONG;
++ final int toBitsetIdx = to >>> LOG2_LONG;
++
++ final long keepFirst = ~(ALL_SET << from);
++ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
++
++ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
++
++ if (fromBitsetIdx == toBitsetIdx) {
++ // special case: need to keep both first and last
++ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
++ } else {
++ bitset[fromBitsetIdx] &= keepFirst;
++
++ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
++ bitset[i] = 0L;
+ }
-+ } finally {
-+ this.entityByLock.tryUnlockWrite();
++
++ bitset[toBitsetIdx] &= keepLast;
+ }
+ }
+
-+ private ChunkEntitySlices moveEntity(final Entity entity) {
-+ // ensure we own the entity
-+ TickThread.ensureTickThread(entity, "Cannot move entity off-main");
++ // from inclusive
++ // to exclusive
++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
++ return firstClear(bitset, from, to) == -1;
++ }
+
-+ final BlockPos newPos = entity.blockPosition();
-+ final int newSectionX = newPos.getX() >> 4;
-+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
-+ final int newSectionZ = newPos.getZ() >> 4;
+
-+ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) {
-+ return null;
-+ }
++ private FlatBitsetUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ac6f284ee4469d16c5655328b2488d7612832353
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -0,0 +1,10 @@
++package ca.spottedleaf.moonrise.common.util;
+
-+ // ensure the new section is owned by this tick thread
-+ TickThread.ensureTickThread(this.world, newSectionX, newSectionZ, "Cannot move entity off-main");
++public final class MixinWorkarounds {
+
-+ // ensure the old section is owned by this tick thread
-+ TickThread.ensureTickThread(this.world, entity.sectionX, entity.sectionZ, "Cannot move entity off-main");
++ // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++ public static long[] clone(final long[] values) {
++ return values.clone();
++ }
+
-+ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ);
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ef1c9e1e8636a14b5215c6c55d3032bacfd94cac
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -0,0 +1,45 @@
++package ca.spottedleaf.moonrise.common.util;
+
-+ if (!old.removeEntity(entity, entity.sectionY)) {
-+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + entity.sectionX + "," + entity.sectionY + "," + entity.sectionZ + ") since it was not contained in the section");
-+ }
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
+
-+ if (!slices.addEntity(entity, newSectionY)) {
-+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section");
++public final class MoonriseCommon {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
++
++ // Paper start
++ public static PrioritisedThreadPool WORKER_POOL;
++ public static int WORKER_THREADS;
++ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
++ // Paper end
++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
++ if (defaultWorkerThreads <= 4) {
++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
++ } else {
++ defaultWorkerThreads = defaultWorkerThreads / 2;
+ }
++ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
+
-+ entity.sectionX = newSectionX;
-+ entity.sectionY = newSectionY;
-+ entity.sectionZ = newSectionZ;
++ int workerThreads = chunkSystem.workerThreads;
+
-+ return slices;
++ if (workerThreads <= 0) {
++ workerThreads = defaultWorkerThreads;
++ }
++
++ WORKER_POOL = new PrioritisedThreadPool(
++ "Paper Worker Pool", workerThreads,
++ (final Thread thread, final Integer id) -> {
++ thread.setName("Paper Common Worker #" + id.intValue());
++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++ @Override
++ public void uncaughtException(final Thread thread, final Throwable throwable) {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ }
++ });
++ }, (long)(20.0e6)); // 20ms
++ WORKER_THREADS = workerThreads;
+ }
+
-+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++ private MoonriseCommon() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.common.util;
+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++public final class MoonriseConstants {
+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++ public static final int MAX_VIEW_DISTANCE = 32;
+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++ private MoonriseConstants() {}
+
-+ if (region == null) {
-+ continue;
-+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e95cc73ddf20050aa4a241b0a309240e2bf46abd
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -0,0 +1,54 @@
++package ca.spottedleaf.moonrise.common.util;
+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelHeightAccessor;
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
++public final class WorldUtil {
+
-+ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
++ // min, max are inclusive
++
++ public static int getMaxSection(final LevelHeightAccessor world) {
++ return world.getMaxSection() - 1; // getMaxSection() is exclusive
+ }
+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++ public static int getMinSection(final LevelHeightAccessor world) {
++ return world.getMinSection();
++ }
+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++ public static int getMaxLightSection(final LevelHeightAccessor world) {
++ return getMaxSection(world) + 1;
++ }
+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++ public static int getMinLightSection(final LevelHeightAccessor world) {
++ return getMinSection(world) - 1;
++ }
+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
-+ if (region == null) {
-+ continue;
-+ }
+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++ public static int getTotalSections(final LevelHeightAccessor world) {
++ return getMaxSection(world) - getMinSection(world) + 1;
++ }
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
++ public static int getTotalLightSections(final LevelHeightAccessor world) {
++ return getMaxLightSection(world) - getMinLightSection(world) + 1;
++ }
+
-+ chunk.getEntities(except, box, into, predicate);
-+ }
-+ }
-+ }
++ public static int getMinBlockY(final LevelHeightAccessor world) {
++ return getMinSection(world) << 4;
++ }
++
++ public static int getMaxBlockY(final LevelHeightAccessor world) {
++ return (getMaxSection(world) << 4) | 15;
++ }
++
++ public static String getWorldName(final Level world) {
++ if (world == null) {
++ return "null world";
+ }
++ return world.getWorld().getName();
+ }
+
-+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++ private WorldUtil() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e690549d08956676d6c2bc463732cc8067000618
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
+@@ -0,0 +1,151 @@
++package ca.spottedleaf.moonrise.patches.chunk_system;
+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
++import com.mojang.logging.LogUtils;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import org.slf4j.Logger;
++import java.util.List;
++import java.util.function.Consumer;
+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++public final class ChunkSystem {
+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++ private static final Logger LOGGER = LogUtils.getLogger();
+
-+ if (region == null) {
-+ continue;
-+ }
++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++ scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++ }
+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
++ }
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
++ final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
++ final Consumer<ChunkAccess> onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
++ }
+
-+ chunk.getHardCollidingEntities(except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
++ // Paper - rewrite chunk system
++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }
+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
++ final FullChunkStatus toStatus, final boolean addTicket,
++ final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }
+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
++ }
+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
++ }
+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++ public static int getVisibleChunkHolderCount(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++ }
+
-+ if (region == null) {
-+ continue;
-+ }
++ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++ }
+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++ public static boolean hasAnyChunkHolders(final ServerLevel level) {
++ return getUpdatingChunkHolderCount(level) != 0;
++ }
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
++ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd(level, entity);
++ }
+
-+ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
-+ }
-+ }
-+ }
-+ }
++ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(level, holder);
+ }
+
-+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(level, holder);
++ }
+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, holder);
++ chunk.loadCallback(); // Paper
++ }
+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(chunk, holder);
++ chunk.unloadCallback(); // Paper
++ }
+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, holder);
++ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
++ chunk.postProcessGeneration();
++ }
++ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
++ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
++ }
+
-+ if (region == null) {
-+ continue;
-+ }
++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(chunk, holder);
++ }
+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, holder);
++ }
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(chunk, holder);
++ }
+
-+ chunk.getEntities(clazz, except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
++ return null;
+ }
+
-+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot load in entity section off-main");
-+ synchronized (this) {
-+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ);
-+ if (curr != null) {
-+ this.removeChunk(chunkX, chunkZ);
++ public static int getSendViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
++ }
+
-+ curr.mergeInto(slices);
++ public static int getLoadViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
++ }
+
-+ this.addChunk(chunkX, chunkZ, slices);
-+ } else {
-+ this.addChunk(chunkX, chunkZ, slices);
-+ }
-+ }
++ public static int getTickViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
+ }
+
-+ public void entitySectionUnload(final int chunkX, final int chunkZ) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot unload entity section off-main");
-+ this.removeChunk(chunkX, chunkZ);
++ public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
+ }
+
-+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
-+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ if (region == null) {
-+ return null;
-+ }
++ public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
++ }
+
-+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
++ public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
+ }
+
-+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
-+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ ChunkEntitySlices ret;
-+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
-+ // loadInEntityChunk will call addChunk for us
-+ return this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(chunkX, chunkZ, true);
-+ }
++ private ChunkSystem() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05959524f3
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+@@ -0,0 +1,38 @@
++package ca.spottedleaf.moonrise.patches.chunk_system;
+
-+ return ret;
++import net.minecraft.SharedConstants;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.util.datafix.DataFixTypes;
++
++public final class ChunkSystemConverters {
++
++ // See SectionStorage#getVersion
++ private static final int DEFAULT_POI_DATA_VERSION = 1945;
++
++ private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1;
++
++ private static int getCurrentVersion() {
++ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
+ }
+
-+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
-+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
-+ final long attempt = this.stateLock.tryOptimisticRead();
-+ if (attempt != 0L) {
-+ try {
-+ final ChunkSlicesRegion ret = this.regions.get(key);
++ private static int getDataVersion(final CompoundTag data, final int dfl) {
++ return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC)
++ ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG);
++ }
+
-+ if (this.stateLock.validate(attempt)) {
-+ return ret;
-+ }
-+ } catch (final Error error) {
-+ throw error;
-+ } catch (final Throwable thr) {
-+ // ignore
-+ }
-+ }
++ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
++ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+
-+ this.stateLock.readLock();
-+ try {
-+ return this.regions.get(key);
-+ } finally {
-+ this.stateLock.tryUnlockRead();
-+ }
++ return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
+ }
+
-+ private synchronized void removeChunk(final int chunkX, final int chunkZ) {
-+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) {
++ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION);
+
-+ final ChunkSlicesRegion region = this.regions.get(key);
-+ final int remaining = region.remove(relIndex);
++ return DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
++ }
+
-+ if (remaining == 0) {
-+ this.stateLock.writeLock();
-+ try {
-+ this.regions.remove(key);
-+ } finally {
-+ this.stateLock.tryUnlockWrite();
-+ }
-+ }
++ private ChunkSystemConverters() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..67f6dd9a4855611cfe242c2e37e90f6d27d4c823
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
+@@ -0,0 +1,36 @@
++package ca.spottedleaf.moonrise.patches.chunk_system;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.chunk.ChunkAccess;
++
++public final class ChunkSystemFeatures {
++
++ public static boolean supportsAsyncChunkSave() {
++ // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write
++ // additionally, there may be mods hooking into the write() call which may not be thread-safe to call
++ return true;
+ }
+
-+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
-+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++ public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) {
++ return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk);
++ }
+
-+ ChunkSlicesRegion region = this.regions.get(key);
-+ if (region != null) {
-+ region.add(relIndex, slices);
-+ } else {
-+ region = new ChunkSlicesRegion();
-+ region.add(relIndex, slices);
-+ this.stateLock.writeLock();
-+ try {
-+ this.regions.put(key, region);
-+ } finally {
-+ this.stateLock.tryUnlockWrite();
-+ }
-+ }
++ public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) {
++ return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData);
+ }
+
-+ public static final class ChunkSlicesRegion {
++ public static boolean forceNoSave(final ChunkAccess chunk) {
++ // support for CB chunk mustNotSave
++ return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave;
++ }
+
-+ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
-+ protected int sliceCount;
++ public static boolean supportsAsyncChunkDeserialization() {
++ // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods
++ // hooking into ChunkSerializer#read() are thread-safe to call
++ return true;
++ }
+
-+ public ChunkEntitySlices get(final int index) {
-+ return this.slices[index];
-+ }
++ private ChunkSystemFeatures() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..becd1c6d54ed6c912aee3a9178a970e2751d3694
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
+@@ -0,0 +1,11 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.async_save;
+
-+ public int remove(final int index) {
-+ final ChunkEntitySlices slices = this.slices[index];
-+ if (slices == null) {
-+ throw new IllegalStateException();
-+ }
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.Tag;
+
-+ this.slices[index] = null;
++public record AsyncChunkSaveData(
++ Tag blockTickList, // non-null if we had to go to the server's tick list
++ Tag fluidTickList, // non-null if we had to go to the server's tick list
++ ListTag blockEntities,
++ long worldTime
++) {}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec4bd9ac4e
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
+@@ -0,0 +1,39 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.entity;
+
-+ return --this.sliceCount;
-+ }
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.monster.Shulker;
++import net.minecraft.world.entity.vehicle.AbstractMinecart;
++import net.minecraft.world.entity.vehicle.Boat;
+
-+ public void add(final int index, final ChunkEntitySlices slices) {
-+ final ChunkEntitySlices curr = this.slices[index];
-+ if (curr != null) {
-+ throw new IllegalStateException();
-+ }
++public interface ChunkSystemEntity {
+
-+ this.slices[index] = slices;
++ public boolean moonrise$isHardColliding();
+
-+ ++this.sliceCount;
-+ }
++ // for mods to override
++ public default boolean moonrise$isHardCollidingUncached() {
++ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith();
+ }
+
-+ private final class EntityCallback implements EntityInLevelCallback {
++ public FullChunkStatus moonrise$getChunkStatus();
+
-+ public final Entity entity;
++ public void moonrise$setChunkStatus(final FullChunkStatus status);
+
-+ public EntityCallback(final Entity entity) {
-+ this.entity = entity;
-+ }
++ public int moonrise$getSectionX();
+
-+ @Override
-+ public void onMove() {
-+ final Entity entity = this.entity;
-+ final Visibility oldVisibility = getEntityStatus(entity);
-+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
-+ if (newSlices == null) {
-+ // no new section, so didn't change sections
-+ return;
-+ }
-+ final Visibility newVisibility = getEntityStatus(entity);
++ public void moonrise$setSectionX(final int x);
+
-+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
-+ }
++ public int moonrise$getSectionY();
+
-+ @Override
-+ public void onRemove(final Entity.RemovalReason reason) {
-+ final Entity entity = this.entity;
-+ TickThread.ensureTickThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system
-+ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
++ public void moonrise$setSectionY(final int y);
+
-+ EntityLookup.this.removeEntity(entity);
++ public int moonrise$getSectionZ();
+
-+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
++ public void moonrise$setSectionZ(final int z);
+
-+ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
-+ }
-+ }
++ public boolean moonrise$isUpdatingSectionStatus();
+
-+ private static final class NoOpCallback implements EntityInLevelCallback {
++ public void moonrise$setUpdatingSectionStatus(final boolean to);
+
-+ public static final NoOpCallback INSTANCE = new NoOpCallback();
++ public boolean moonrise$hasAnyPlayerPassengers();
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c792ed3dd1d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
+@@ -0,0 +1,14 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.io;
+
-+ @Override
-+ public void onMove() {}
++import net.minecraft.world.level.chunk.storage.RegionFile;
++import java.io.IOException;
++
++public interface ChunkSystemRegionFileStorage {
++
++ public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ);
++
++ public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ);
++
++ public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException;
+
-+ @Override
-+ public void onRemove(final Entity.RemovalReason reason) {}
-+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
new file mode 100644
-index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aac1ec60e8
+index 0000000000000000000000000000000000000000..c833f78d083b8f661087471c35bc90f65af1b525
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java
-@@ -0,0 +1,1289 @@
-+package io.papermc.paper.chunk.system.io;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
+@@ -0,0 +1,1239 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.io;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
++import ca.spottedleaf.concurrentutil.function.BiLong1Function;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import it.unimi.dsi.fastutil.HashCommon;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.RegionFile;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.lang.invoke.VarHandle;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
-+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
-+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Prioritised RegionFile I/O executor, responsible for all RegionFile access.
+ * <p>
-+ * All functions provided are MT-Safe, however certain ordering constraints are recommended:
-+ * <ul>
++ * All functions provided are MT-Safe, however certain ordering constraints are recommended:
+ * <li>
+ * Chunk saves may not occur for unloaded chunks.
+ * </li>
+ * <li>
+ * Tasks must be scheduled on the chunk scheduler thread.
+ * </li>
-+ * </ul>
-+ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
++ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
++ * </p>
+ */
+public final class RegionFileIOThread extends PrioritisedQueueExecutorThread {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class);
+
+ /**
+ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure
@@ -3580,15 +3490,19 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ ENTITY_DATA;
+ }
+
-+ protected static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
+
-+ private ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
-+ return switch (type) {
-+ case CHUNK_DATA -> world.chunkDataControllerNew;
-+ case POI_DATA -> world.poiDataControllerNew;
-+ case ENTITY_DATA -> world.entityDataControllerNew;
-+ default -> throw new IllegalStateException("Unknown controller type " + type);
-+ };
++ public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
++ switch (type) {
++ case CHUNK_DATA:
++ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController();
++ case POI_DATA:
++ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController();
++ case ENTITY_DATA:
++ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController();
++ default:
++ throw new IllegalStateException("Unknown controller type " + type);
++ }
+ }
+
+ /**
@@ -3739,6 +3653,12 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+ }
+
++ public static void flushRegionStorages(final ServerLevel world) throws IOException {
++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
++ getControllerFor(world, type).getCache().flush();
++ }
++ }
++
+ public static void partialFlush(final int totalTasksRemaining) {
+ long failures = 1L; // start out at 0.25ms
+
@@ -3779,6 +3699,16 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+ }
+
++ public static void deinit() {
++ if (true) { // Paper
++ // TODO does this cause issues with mods? how to implement
++ close(true);
++ synchronized (INIT_LOCK) {
++ RegionFileIOThread.threads = null;
++ }
++ } else { RegionFileIOThread.flush(); }
++ }
++
+ private RegionFileIOThread(final int threadNumber) {
+ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time
+ this.setName("RegionFile I/O Thread #" + threadNumber);
@@ -3801,15 +3731,15 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * dumb plugins from taking away priority from threads we consider crucial.
+ * @return The priroity to use with blocking I/O on the current thread.
+ */
-+ public static PrioritisedExecutor.Priority getIOBlockingPriorityForCurrentThread() {
-+ if (TickThread.isTickThread()) {
-+ return PrioritisedExecutor.Priority.BLOCKING;
++ public static Priority getIOBlockingPriorityForCurrentThread() {
++ if (io.papermc.paper.util.TickThread.isTickThread()) {
++ return Priority.BLOCKING;
+ }
-+ return PrioritisedExecutor.Priority.HIGHEST;
++ return Priority.HIGHEST;
+ }
+
+ /**
-+ * Returns the current {@code CompoundTag} pending for write for the specified chunk and regionfile type.
++ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type.
+ * Note that this does not copy the result, so do not modify the result returned.
+ *
+ * @param world Specified world.
@@ -3825,8 +3755,8 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+
+ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ)));
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task == null) {
+ return null;
@@ -3845,17 +3775,17 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @param type Specified regionfile type.
+ * @return The priority for the chunk
+ */
-+ public static PrioritisedExecutor.Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
++ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ return thread.getPriorityInternal(world, chunkX, chunkZ, type);
+ }
+
-+ PrioritisedExecutor.Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ)));
++ Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task == null) {
-+ return PrioritisedExecutor.Priority.COMPLETING;
++ return Priority.COMPLETING;
+ }
+
+ return task.prioritisedTask.getPriority();
@@ -3877,7 +3807,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority);
+ }
@@ -3900,15 +3830,15 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ)));
++ final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.setPriority(priority);
@@ -3929,7 +3859,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority);
+ }
@@ -3950,15 +3880,15 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ)));
++ final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.raisePriority(priority);
@@ -3979,7 +3909,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority);
+ }
@@ -4000,15 +3930,15 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ)));
++ final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.lowerPriority(priority);
@@ -4018,16 +3948,15 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * <li>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
++ * <li>
+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ * </ul>
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4039,22 +3968,21 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type) {
-+ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, PrioritisedExecutor.Priority.NORMAL);
++ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
+ }
+
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * <li>
-+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
++ * <li>
++ * Writes may be called concurrently, although only the "later" write will go through.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4066,18 +3994,18 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @throws IllegalStateException If the file io thread has shutdown.
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-+ final RegionFileType type, final PrioritisedExecutor.Priority priority) {
++ final RegionFileType type, final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority);
+ }
+
+ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-+ final RegionFileType type, final PrioritisedExecutor.Priority priority) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
++ final RegionFileType type, final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
+
+ final boolean[] created = new boolean[1];
-+ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ final ChunkDataTask task = taskController.tasks.compute(key, (final ChunkCoordinate keyInMap, final ChunkDataTask taskRunning) -> {
++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> {
+ if (taskRunning == null || taskRunning.failedWrite) {
+ // no task is scheduled or the previous write failed - meaning we need to overwrite it
+
@@ -4106,14 +4034,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
+ * for single load.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4131,7 +4058,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ */
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) {
-+ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL);
++ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
@@ -4139,14 +4066,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
+ * for single load.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4165,7 +4091,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ */
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
+ }
+
@@ -4174,14 +4100,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
+ * for single load.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4201,7 +4126,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final RegionFileType... types) {
-+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL, types);
++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
+ }
+
+ /**
@@ -4209,14 +4134,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
+ * for single load.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4236,7 +4160,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ */
+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
-+ final PrioritisedExecutor.Priority priority, final RegionFileType... types) {
++ final Priority priority, final RegionFileType... types) {
+ if (types == null) {
+ throw new NullPointerException("Types cannot be null");
+ }
@@ -4273,14 +4197,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
+ * {@code onComplete}.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4299,21 +4222,20 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
+ final boolean intendingToBlock) {
-+ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL);
++ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
+ * {@code onComplete}.
+ * <p>
-+ * Impl notes:
-+ * <ul>
-+ * <li>
-+ * 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.
-+ * </li>
-+ * </ul>
++ * Impl notes:
++ * </p>
++ * <li>
++ * 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.
++ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
@@ -4332,20 +4254,20 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ */
+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) {
++ final boolean intendingToBlock, final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority);
+ }
+
+ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) {
-+ final ChunkDataController taskController = this.getControllerFor(world, type);
++ final boolean intendingToBlock, final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
+
+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
+
-+ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ final BiFunction<ChunkCoordinate, ChunkDataTask, ChunkDataTask> compute = (final ChunkCoordinate keyInMap, final ChunkDataTask running) -> {
++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> {
+ if (running == null) {
+ // not scheduled
+
@@ -4353,8 +4275,8 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ final ChunkDataTask newTask = new ChunkDataTask(
+ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority
+ );
-+ newTask.inProgressRead = new RegionFileIOThread.InProgressRead();
-+ newTask.inProgressRead.waiters.add(onComplete);
++ newTask.inProgressRead = new InProgressRead();
++ newTask.inProgressRead.addToAsyncWaiters(onComplete);
+
+ callbackInfo.tasksNeedsScheduling = true;
+ return newTask;
@@ -4364,14 +4286,13 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+
+ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) {
+ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations
-+ if (!running.inProgressRead.addToWaiters(onComplete)) {
++ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) {
+ callbackInfo.data = running.inProgressRead.value;
+ callbackInfo.throwable = running.inProgressRead.throwable;
+ callbackInfo.completeNow = true;
+ }
+ return running;
+ }
-+ // using the result sync here - don't bump priority
+
+ // at this stage we have to use the in progress write's data to avoid an order issue
+ callbackInfo.data = pendingWrite;
@@ -4387,9 +4308,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ ret.prioritisedTask.queue();
+ } else if (callbackInfo.completeNow) {
+ try {
-+ onComplete.accept(callbackInfo.data, callbackInfo.throwable);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
++ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable);
+ } catch (final Throwable thr) {
+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr);
+ }
@@ -4415,7 +4334,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * @throws IOException If the load fails for any reason
+ */
+ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) throws IOException {
++ final Priority priority) throws IOException {
+ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>();
+
+ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
@@ -4442,12 +4361,12 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+
+ }
+
-+ static final class CancellableRead implements Cancellable {
++ private static final class CancellableRead implements Cancellable {
+
+ private BiConsumer<CompoundTag, Throwable> callback;
-+ private RegionFileIOThread.ChunkDataTask task;
++ private ChunkDataTask task;
+
-+ CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final RegionFileIOThread.ChunkDataTask task) {
++ CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) {
+ this.callback = callback;
+ this.task = task;
+ }
@@ -4455,7 +4374,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ @Override
+ public boolean cancel() {
+ final BiConsumer<CompoundTag, Throwable> callback = this.callback;
-+ final RegionFileIOThread.ChunkDataTask task = this.task;
++ final ChunkDataTask task = this.task;
+
+ if (callback == null || task == null) {
+ return false;
@@ -4464,18 +4383,18 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ this.callback = null;
+ this.task = null;
+
-+ final RegionFileIOThread.InProgressRead read = task.inProgressRead;
++ final InProgressRead read = task.inProgressRead;
+
+ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't)
-+ return (read != null && read.waiters.remove(callback));
++ return read != null && read.cancel(callback);
+ }
+ }
+
-+ static final class CancellableReads implements Cancellable {
++ private static final class CancellableReads implements Cancellable {
+
+ private Cancellable[] reads;
+
-+ protected static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
++ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
+
+ CancellableReads(final Cancellable[] reads) {
+ this.reads = reads;
@@ -4499,29 +4418,34 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+ }
+
-+ static final class InProgressRead {
++ private static final class InProgressRead {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private CompoundTag value;
++ private Throwable throwable;
++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
+
-+ CompoundTag value;
-+ Throwable throwable;
-+ final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> waiters = new MultiThreadedQueue<>();
++ public boolean hasNoWaiters() {
++ return this.callbacks.isEmpty();
++ }
++
++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
++ return this.callbacks.add(callback);
++ }
+
-+ // rets false if already completed (callback not invoked), true if callback was added
-+ boolean addToWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.waiters.add(callback);
++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
++ return this.callbacks.remove(callback);
+ }
+
-+ void complete(final RegionFileIOThread.ChunkDataTask task, final CompoundTag value, final Throwable throwable) {
++ public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) {
+ this.value = value;
+ this.throwable = throwable;
+
+ BiConsumer<CompoundTag, Throwable> consumer;
-+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
+ try {
-+ consumer.accept(value, throwable);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
++ consumer.accept(value == null ? null : value.copy(), throwable);
+ } catch (final Throwable thr) {
+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr);
+ }
@@ -4529,58 +4453,10 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+ }
+
-+ /**
-+ * Class exists to replace {@link Long} usages as keys inside non-fastutil hashtables. The hash for some Long {@code x}
-+ * is defined as {@code (x >>> 32) ^ x}. Chunk keys as long values are defined as {@code ((chunkX & 0xFFFFFFFFL) | (chunkZ << 32))},
-+ * which means the hashcode as a Long value will be {@code chunkX ^ chunkZ}. Given that most chunks are created within a radius arounds players,
-+ * this will lead to many hash collisions. So, this class uses a better hashing algorithm so that usage of
-+ * non-fastutil collections is not degraded.
-+ */
-+ public static final class ChunkCoordinate implements Comparable<ChunkCoordinate> {
-+
-+ public final long key;
-+
-+ public ChunkCoordinate(final long key) {
-+ this.key = key;
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ return (int)HashCommon.mix(this.key);
-+ }
-+
-+ @Override
-+ public boolean equals(final Object obj) {
-+ if (this == obj) {
-+ return true;
-+ }
-+
-+ if (!(obj instanceof ChunkCoordinate)) {
-+ return false;
-+ }
-+
-+ final ChunkCoordinate other = (ChunkCoordinate)obj;
-+
-+ return this.key == other.key;
-+ }
-+
-+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain
-+ // is too large. So we should implement compareTo to help.
-+ @Override
-+ public int compareTo(final RegionFileIOThread.ChunkCoordinate other) {
-+ return Long.compare(this.key, other.key);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return new ChunkPos(this.key).toString();
-+ }
-+ }
-+
+ public static abstract class ChunkDataController {
+
+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
-+ protected final ConcurrentHashMap<ChunkCoordinate, ChunkDataTask> tasks = new ConcurrentHashMap<>(8192, 0.10f);
++ private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f);
+
+ public final RegionFileType type;
+
@@ -4599,7 +4475,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+
+ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) {
-+ return this.getCache().doesRegionFileNotExistNoIO(new ChunkPos(chunkX, chunkZ));
++ return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ);
+ }
+
+ public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) {
@@ -4607,18 +4483,16 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ final RegionFile regionFile;
+ synchronized (cache) {
+ try {
-+ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly, true);
++ if (existingOnly) {
++ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ);
++ } else {
++ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly);
++ }
+ } catch (final IOException ex) {
+ throw new RuntimeException(ex);
+ }
-+ }
+
-+ try {
+ return function.apply(regionFile);
-+ } finally {
-+ if (regionFile != null) {
-+ regionFile.fileLock.unlock();
-+ }
+ }
+ }
+
@@ -4627,39 +4501,30 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ final RegionFile regionFile;
+
+ synchronized (cache) {
-+ regionFile = cache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ));
-+ if (regionFile != null) {
-+ regionFile.fileLock.lock();
-+ }
-+ }
++ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ);
+
-+ try {
+ return function.apply(regionFile);
-+ } finally {
-+ if (regionFile != null) {
-+ regionFile.fileLock.unlock();
-+ }
+ }
+ }
+ }
+
-+ static final class ChunkDataTask implements Runnable {
++ private static final class ChunkDataTask implements Runnable {
+
-+ protected static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
++ private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class);
+
-+ RegionFileIOThread.InProgressRead inProgressRead;
-+ volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
++ private InProgressRead inProgressRead;
++ private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
+
-+ boolean failedWrite;
++ private boolean failedWrite;
+
-+ final ServerLevel world;
-+ final int chunkX;
-+ final int chunkZ;
-+ final RegionFileIOThread.ChunkDataController taskController;
++ private final ServerLevel world;
++ private final int chunkX;
++ private final int chunkZ;
++ private final ChunkDataController taskController;
+
-+ final PrioritisedExecutor.PrioritisedTask prioritisedTask;
++ private final PrioritisedTask prioritisedTask;
+
+ /*
+ * IO thread will perform reads before writes for a given chunk x and z
@@ -4677,8 +4542,8 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ * it fails to properly propagate write failures thanks to writes overwriting each other
+ */
+
-+ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileIOThread.ChunkDataController taskController,
-+ final PrioritisedExecutor executor, final PrioritisedExecutor.Priority priority) {
++ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController,
++ final PrioritisedExecutor executor, final Priority priority) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
@@ -4688,21 +4553,21 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+
+ @Override
+ public String toString() {
-+ return "Task for world: '" + this.world.getWorld().getName() + "' at (" + this.chunkX + "," + this.chunkZ +
++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ +
+ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode();
+ }
+
+ @Override
+ public void run() {
-+ final RegionFileIOThread.InProgressRead read = this.inProgressRead;
-+ final ChunkCoordinate chunkKey = new ChunkCoordinate(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
++ final InProgressRead read = this.inProgressRead;
++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
+
+ if (read != null) {
+ final boolean[] canRead = new boolean[] { true };
+
-+ if (read.waiters.isEmpty()) {
++ if (read.hasNoWaiters()) {
+ // cancelled read? go to task controller to confirm
-+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> {
++ final 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!");
+ }
@@ -4710,7 +4575,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+
-+ if (!read.waiters.isEmpty()) { // as per usual IntelliJ is unable to figure out that there are concurrent accesses.
++ if (!read.hasNoWaiters()) {
+ return valueInMap;
+ } else {
+ canRead[0] = false;
@@ -4734,8 +4599,6 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+
+ try {
+ compound = this.taskController.readData(this.chunkX, this.chunkZ);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ throwable = thr;
+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
@@ -4747,7 +4610,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ CompoundTag write = this.inProgressWrite;
+
+ if (write == NOTHING_TO_WRITE) {
-+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> {
++ final 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!");
+ }
@@ -4770,12 +4633,10 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+
+ try {
+ this.taskController.writeData(this.chunkX, this.chunkZ, write);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ if (thr instanceof RegionFileStorage.RegionFileSizeException) {
+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
-+ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
+ } else {
+ failedWrite = thr instanceof IOException;
+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
@@ -4785,7 +4646,7 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ final boolean finalFailWrite = failedWrite;
+ final boolean[] done = new boolean[] { false };
+
-+ this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> {
++ 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!");
+ }
@@ -4812,309 +4673,2496 @@ index 0000000000000000000000000000000000000000..2096e57c025858519e7c46788993b9aa
+ }
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db224766ef17b7
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
+@@ -0,0 +1,56 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.storage.RegionFileStorage;
++import java.io.IOException;
++import java.util.Optional;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.CompletionException;
++
++public final class ChunkDataController extends RegionFileIOThread.ChunkDataController {
++
++ private final ServerLevel world;
++
++ public ChunkDataController(final ServerLevel world) {
++ super(RegionFileIOThread.RegionFileType.CHUNK_DATA);
++ this.world = world;
++ }
++
++ @Override
++ public RegionFileStorage getCache() {
++ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage();
++ }
++
++ @Override
++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
++ final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound);
++
++ try {
++ if (future != null) {
++ // rets non-null when sync writing (i.e. future should be completed here)
++ future.join();
++ }
++ } catch (final CompletionException ex) {
++ if (ex.getCause() instanceof IOException ioException) {
++ throw ioException;
++ }
++ throw ex;
++ }
++ }
++
++ @Override
++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
++ try {
++ return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null);
++ } catch (final CompletionException ex) {
++ if (ex.getCause() instanceof IOException ioException) {
++ throw ioException;
++ }
++ throw ex;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
new file mode 100644
-index 0000000000000000000000000000000000000000..69e9944358951bd69ff5e8b3482da1a5e4476209
+index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab717aecf60
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java
-@@ -0,0 +1,283 @@
-+package io.papermc.paper.chunk.system.light;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
+@@ -0,0 +1,55 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.storage.EntityStorage;
++import net.minecraft.world.level.chunk.storage.RegionFileStorage;
++import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
++import java.io.IOException;
++import java.nio.file.Path;
++
++public final class EntityDataController extends RegionFileIOThread.ChunkDataController {
++
++ private final EntityRegionFileStorage storage;
++
++ public EntityDataController(final EntityRegionFileStorage storage) {
++ super(RegionFileIOThread.RegionFileType.ENTITY_DATA);
++ this.storage = storage;
++ }
++
++ @Override
++ public RegionFileStorage getCache() {
++ return this.storage;
++ }
++
++ @Override
++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
++ this.storage.write(new ChunkPos(chunkX, chunkZ), compound);
++ }
++
++ @Override
++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
++ return this.storage.read(new ChunkPos(chunkX, chunkZ));
++ }
++
++ public static final class EntityRegionFileStorage extends RegionFileStorage {
++
++ public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory,
++ final boolean dsync) {
++ super(regionStorageInfo, directory, dsync);
++ }
++
++ @Override
++ public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException {
++ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
++ if (nbtPos != null && !pos.equals(nbtPos)) {
++ throw new IllegalArgumentException(
++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
++ + " but compound says coordinate is " + nbtPos + " for world: " + this
++ );
++ }
++ super.write(pos, nbt);
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f17363483b
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
+@@ -0,0 +1,33 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.chunk.storage.RegionFileStorage;
++import java.io.IOException;
++
++public final class PoiDataController extends RegionFileIOThread.ChunkDataController {
++
++ private final ServerLevel world;
++
++ public PoiDataController(final ServerLevel world) {
++ super(RegionFileIOThread.RegionFileType.POI_DATA);
++ this.world = world;
++ }
++
++ @Override
++ public RegionFileStorage getCache() {
++ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage();
++ }
++
++ @Override
++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
++ ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound);
++ }
++
++ @Override
++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
++ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..eab09949c001fbfd708079fae83c45ab59fb25e7
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
+@@ -0,0 +1,20 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++
++public interface ChunkSystemLevel {
++
++ public EntityLookup moonrise$getEntityLookup();
++
++ public void moonrise$setEntityLookup(final EntityLookup entityLookup);
++
++ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
++
++ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ);
++
++ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
+@@ -0,0 +1,10 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level;
++
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++
++public interface ChunkSystemLevelReader {
++
++ public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d0d97588e02a7846ef9da57679a9ca4525daee17
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
+@@ -0,0 +1,47 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.starlight.common.light.BlockStarLightEngine;
-+import ca.spottedleaf.starlight.common.light.SkyStarLightEngine;
-+import ca.spottedleaf.starlight.common.light.StarLightInterface;
-+import io.papermc.paper.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import java.util.List;
++import java.util.function.Consumer;
++
++public interface ChunkSystemServerLevel extends ChunkSystemLevel {
++
++ public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
++
++ public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController();
++
++ public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController();
++
++ public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController();
++
++ public int moonrise$getRegionChunkShift();
++
++ // Paper - marked closing not needed on CB
++
++ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
++
++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
++ final PrioritisedExecutor.Priority priority,
++ final Consumer<List<ChunkAccess>> onLoad);
++
++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
++ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
++ final Consumer<List<ChunkAccess>> onLoad);
++
++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
++ final PrioritisedExecutor.Priority priority,
++ final Consumer<List<ChunkAccess>> onLoad);
++
++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
++ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
++ final Consumer<List<ChunkAccess>> onLoad);
++
++ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
+@@ -0,0 +1,26 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.level.chunk.LevelChunk;
++import java.util.List;
++
++public interface ChunkSystemChunkHolder {
++
++ public NewChunkHolder moonrise$getRealChunkHolder();
++
++ public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder);
++
++ public void moonrise$addReceivedChunk(final ServerPlayer player);
++
++ public void moonrise$removeReceivedChunk(final ServerPlayer player);
++
++ public boolean moonrise$hasChunkBeenSent();
++
++ public boolean moonrise$hasChunkBeenSent(final ServerPlayer to);
++
++ public List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge);
++
++ public LevelChunk moonrise$getFullChunk();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
+@@ -0,0 +1,26 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
++
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import java.util.concurrent.atomic.AtomicBoolean;
++
++public interface ChunkSystemChunkStatus {
++
++ public boolean moonrise$isParallelCapable();
++
++ public void moonrise$setParallelCapable(final boolean value);
++
++ public int moonrise$getWriteRadius();
++
++ public void moonrise$setWriteRadius(final int value);
++
++ public ChunkStatus moonrise$getNextStatus();
++
++ public boolean moonrise$isEmptyLoadStatus();
++
++ public void moonrise$setEmptyLoadStatus(final boolean value);
++
++ public boolean moonrise$isEmptyGenStatus();
++
++ public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..883fe6401f1b9711fa544d18a815b4d638f580df
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
++
++import net.minecraft.server.level.ChunkMap;
++
++public interface ChunkSystemDistanceManager {
++
++ public ChunkMap moonrise$getChunkMap();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..755b08dd32e568d341ceef8a8aef841831a0781d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
+@@ -0,0 +1,7 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
++
++public interface ChunkSystemLevelChunk {
++
++ public boolean moonrise$isPostProcessingDone();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f85820b959213c9bb566897c173f644fd430d01a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
+@@ -0,0 +1,810 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
++
++import ca.spottedleaf.moonrise.common.list.EntityList;
++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
++import com.google.common.collect.ImmutableList;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.NbtUtils;
++import net.minecraft.nbt.Tag;
++import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.entity.boss.EnderDragonPart;
++import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.chunk.storage.EntityStorage;
++import net.minecraft.world.level.entity.Visibility;
++import net.minecraft.world.phys.AABB;
+import java.util.ArrayList;
-+import java.util.HashSet;
++import java.util.Arrays;
++import java.util.Iterator;
+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.function.BooleanSupplier;
++import java.util.function.Predicate;
++
++public final class ChunkEntitySlices {
+
-+public final class LightQueue {
++ public final int minSection;
++ public final int maxSection;
++ public final int chunkX;
++ public final int chunkZ;
++ public final Level world;
+
-+ protected final Long2ObjectOpenHashMap<ChunkTasks> chunkTasks = new Long2ObjectOpenHashMap<>();
-+ protected final StarLightInterface manager;
-+ protected final ServerLevel world;
++ private final EntityCollectionBySection allEntities;
++ private final EntityCollectionBySection hardCollidingEntities;
++ private final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
++ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType;
++ private final EntityList entities = new EntityList();
++
++ public FullChunkStatus status;
++
++ private boolean isTransient;
+
-+ public LightQueue(final StarLightInterface manager) {
-+ this.manager = manager;
-+ this.world = ((ServerLevel)manager.getWorld());
++ public boolean isTransient() {
++ return this.isTransient;
+ }
+
-+ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ final ChunkTasks task;
-+ synchronized (this) {
-+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ public void setTransient(final boolean value) {
++ this.isTransient = value;
++ }
++
++ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
++ final int minSection, final int maxSection) { // inclusive, inclusive
++ this.minSection = minSection;
++ this.maxSection = maxSection;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.world = world;
++
++ this.allEntities = new EntityCollectionBySection(this);
++ this.hardCollidingEntities = new EntityCollectionBySection(this);
++ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
++ this.entitiesByType = new Reference2ObjectOpenHashMap<>();
++
++ this.status = status;
++ }
++
++ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) {
++ // TODO check this and below on update for format changes
++ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world).collect(ImmutableList.toImmutableList());
++ }
++
++ // Paper start - rewrite chunk system
++ public static void copyEntities(final CompoundTag from, final CompoundTag into) {
++ if (from == null) {
++ return;
+ }
-+ if (task != null) {
-+ task.lowerPriority(priority);
++ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND);
++ if (entitiesFrom == null || entitiesFrom.isEmpty()) {
++ return;
+ }
++
++ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND);
++ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities
++ entitiesInto.addAll(0, entitiesFrom);
+ }
+
-+ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ final ChunkTasks task;
-+ synchronized (this) {
-+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) {
++ return saveEntityChunk0(entities, chunkPos, world, false);
++ }
++
++ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) {
++ if (!force && entities.isEmpty()) {
++ return null;
+ }
-+ if (task != null) {
-+ task.setPriority(priority);
++
++ final ListTag entitiesTag = new ListTag();
++ for (final Entity entity : entities) {
++ CompoundTag compoundTag = new CompoundTag();
++ if (entity.save(compoundTag)) {
++ entitiesTag.add(compoundTag);
++ }
+ }
++ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag());
++ ret.put("Entities", entitiesTag);
++ EntityStorage.writeChunkPos(ret, chunkPos);
++
++ return !force && entitiesTag.isEmpty() ? null : ret;
+ }
+
-+ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ final ChunkTasks task;
-+ synchronized (this) {
-+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ public CompoundTag save() {
++ final int len = this.entities.size();
++ if (len == 0) {
++ return null;
+ }
-+ if (task != null) {
-+ task.raisePriority(priority);
++
++ final Entity[] rawData = this.entities.getRawData();
++ final List<Entity> collectedEntities = new ArrayList<>(len);
++ for (int i = 0; i < len; ++i) {
++ final Entity entity = rawData[i];
++ if (entity.shouldBeSaved()) {
++ collectedEntities.add(entity);
++ }
+ }
++
++ if (collectedEntities.isEmpty()) {
++ return null;
++ }
++
++ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world);
+ }
+
-+ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) {
-+ final ChunkTasks task;
-+ synchronized (this) {
-+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ // returns true if this chunk has transient entities remaining
++ public boolean unload() {
++ final int len = this.entities.size();
++ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
++
++ for (int i = 0; i < len; ++i) {
++ final Entity entity = collectedEntities[i];
++ if (entity.isRemoved()) {
++ // removed by us below
++ continue;
++ }
++ if (entity.shouldBeSaved()) {
++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
++ if (entity.isVehicle()) {
++ // we cannot assume that these entities are contained within this chunk, because entities can
++ // desync - so we need to remove them all
++ for (final Entity passenger : entity.getIndirectPassengers()) {
++ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
++ }
++ }
++ }
+ }
-+ if (task != null) {
-+ return task.getPriority();
++
++ return this.entities.size() != 0;
++ }
++
++ // Paper start
++ public org.bukkit.entity.Entity[] getChunkEntities() {
++ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ if (entity == null) {
++ continue;
++ }
++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
++ if (bukkit != null && bukkit.isValid()) {
++ ret.add(bukkit);
++ }
++ }
++
++ return ret.toArray(new org.bukkit.entity.Entity[0]);
++ }
++ // Paper end
++
++ private List<Entity> getAllEntities() {
++ final int len = this.entities.size();
++ if (len == 0) {
++ return new ArrayList<>();
+ }
+
-+ return PrioritisedExecutor.Priority.COMPLETING;
++ final Entity[] rawData = this.entities.getRawData();
++ final List<Entity> collectedEntities = new ArrayList<>(len);
++ for (int i = 0; i < len; ++i) {
++ collectedEntities.add(rawData[i]);
++ }
++
++ return collectedEntities;
+ }
+
+ public boolean isEmpty() {
-+ synchronized (this) {
-+ return this.chunkTasks.isEmpty();
++ return this.entities.size() == 0;
++ }
++
++ public void mergeInto(final ChunkEntitySlices slices) {
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY());
+ }
+ }
+
-+ public ChunkTasks queueBlockChange(final BlockPos pos) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
-+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this);
-+ });
-+ tasks.changedPositions.add(pos.immutable());
++ private boolean preventStatusUpdates;
++ public boolean startPreventingStatusUpdates() {
++ final boolean ret = this.preventStatusUpdates;
++ this.preventStatusUpdates = true;
++ return ret;
++ }
++
++ public boolean isPreventingStatusUpdates() {
++ return this.preventStatusUpdates;
++ }
++
++ public void stopPreventingStatusUpdates(final boolean prev) {
++ this.preventStatusUpdates = prev;
++ }
++
++ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) {
++ this.status = status;
++
++ final Entity[] entities = this.entities.getRawData();
++
++ for (int i = 0, size = this.entities.size(); i < size; ++i) {
++ final Entity entity = entities[i];
++
++ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity);
++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status);
++ final Visibility newVisibility = EntityLookup.getEntityStatus(entity);
++
++ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false);
++ }
++ }
++
++ public boolean addEntity(final Entity entity, final int chunkSection) {
++ if (!this.entities.add(entity)) {
++ return false;
++ }
++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status);
++ final int sectionIndex = chunkSection - this.minSection;
++
++ this.allEntities.addEntity(entity, sectionIndex);
++
++ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
++ this.hardCollidingEntities.addEntity(entity, sectionIndex);
++ }
++
++ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
++
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().addEntity(entity, sectionIndex);
++ }
+ }
+
-+ tasks.schedule();
++ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
++ if (byType != null) {
++ byType.addEntity(entity, sectionIndex);
++ } else {
++ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this));
++ byType.addEntity(entity, sectionIndex);
++ }
+
-+ return tasks;
++ return true;
+ }
+
-+ public ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
-+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this);
-+ });
++ public boolean removeEntity(final Entity entity, final int chunkSection) {
++ if (!this.entities.remove(entity)) {
++ return false;
++ }
++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
++ final int sectionIndex = chunkSection - this.minSection;
++
++ this.allEntities.removeEntity(entity, sectionIndex);
++
++ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
++ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
++ }
++
++ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
-+ if (tasks.changedSectionSet == null) {
-+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1];
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().removeEntity(entity, sectionIndex);
+ }
-+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue);
+ }
+
-+ tasks.schedule();
++ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
++ byType.removeEntity(entity, sectionIndex);
+
-+ return tasks;
++ return true;
+ }
+
-+ public ChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
-+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this, priority);
-+ });
-+ if (tasks.lightTasks == null) {
-+ tasks.lightTasks = new ArrayList<>();
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ this.hardCollidingEntities.getEntities(except, box, into, predicate);
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
++ }
++
++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ this.allEntities.getEntities(except, box, into, predicate);
++ }
++
++
++ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
++ final int maxCount) {
++ return this.allEntities.getEntitiesWithEnderDragonPartsLimited(except, box, into, predicate, maxCount);
++ }
++
++ public boolean getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
++ final int maxCount) {
++ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount);
++ }
++
++ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate) {
++ final EntityCollectionBySection byType = this.entitiesByType.get(type);
++
++ if (byType != null) {
++ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate);
++ }
++ }
++
++ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate, final int maxCount) {
++ final EntityCollectionBySection byType = this.entitiesByType.get(type);
++
++ if (byType != null) {
++ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount);
++ }
++
++ return false;
++ }
++
++ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
++ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
++
++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
++ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
++ if (sectionEntities == null) {
++ continue;
++ }
++
++ final Entity[] storage = sectionEntities.storage;
++
++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (clazz.isInstance(entity)) {
++ ret.addEntity(entity, sectionIndex);
++ }
+ }
-+ tasks.lightTasks.add(lightTask);
+ }
+
-+ tasks.schedule();
++ return ret;
++ }
++
++ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate) {
++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
++ if (collection != null) {
++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ } else {
++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ }
++ }
+
-+ return tasks;
++ public <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate, final int maxCount) {
++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
++ if (collection != null) {
++ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
++ } else {
++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
++ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
++ }
+ }
+
-+ public ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
-+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this);
-+ });
++ private static final class BasicEntityList<E extends Entity> {
++
++ private static final Entity[] EMPTY = new Entity[0];
++ private static final int DEFAULT_CAPACITY = 4;
+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet();
++ private E[] storage;
++ private int size;
++
++ public BasicEntityList() {
++ this(0);
++ }
++
++ public BasicEntityList(final int cap) {
++ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
++ }
++
++ public boolean isEmpty() {
++ return this.size == 0;
++ }
++
++ public int size() {
++ return this.size;
++ }
++
++ private void resize() {
++ if (this.storage == EMPTY) {
++ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
++ } else {
++ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
++ }
++ }
++
++ public void add(final E entity) {
++ final int idx = this.size++;
++ if (idx >= this.storage.length) {
++ this.resize();
++ this.storage[idx] = entity;
++ } else {
++ this.storage[idx] = entity;
+ }
-+ queuedEdges.addAll(sections);
+ }
+
-+ tasks.schedule();
++ public int indexOf(final E entity) {
++ final E[] storage = this.storage;
++
++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
++ if (storage[i] == entity) {
++ return i;
++ }
++ }
+
-+ return tasks;
++ return -1;
++ }
++
++ public boolean remove(final E entity) {
++ final int idx = this.indexOf(entity);
++ if (idx == -1) {
++ return false;
++ }
++
++ final int size = --this.size;
++ final E[] storage = this.storage;
++ if (idx != size) {
++ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
++ }
++
++ storage[size] = null;
++
++ return true;
++ }
++
++ public boolean has(final E entity) {
++ return this.indexOf(entity) != -1;
++ }
+ }
+
-+ public ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks;
++ private static final class EntityCollectionBySection {
+
-+ synchronized (this) {
-+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
-+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this);
-+ });
++ private final ChunkEntitySlices slices;
++ private final BasicEntityList<Entity>[] entitiesBySection;
++ private int count;
++
++ public EntityCollectionBySection(final ChunkEntitySlices slices) {
++ this.slices = slices;
++
++ final int sectionCount = slices.maxSection - slices.minSection + 1;
++
++ this.entitiesBySection = new BasicEntityList[sectionCount];
++ }
++
++ public void addEntity(final Entity entity, final int sectionIndex) {
++ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
++
++ if (list != null && list.has(entity)) {
++ return;
++ }
++
++ if (list == null) {
++ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
++ }
++
++ list.add(entity);
++ ++this.count;
++ }
++
++ public void removeEntity(final Entity entity, final int sectionIndex) {
++ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
++
++ if (list == null || !list.remove(entity)) {
++ return;
++ }
++
++ --this.count;
++
++ if (list.isEmpty()) {
++ this.entitiesBySection[sectionIndex] = null;
++ }
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(entity)) {
++ continue;
++ }
++
++ into.add(entity);
++ }
++ }
++ }
++
++ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
++ final int maxCount) {
++ if (this.count == 0) {
++ return false;
++ }
++
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(entity)) {
++ continue;
++ }
++
++ into.add(entity);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List<Entity> into,
++ final Predicate<? super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
++ if (part == except || !part.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet();
++ into.add(part);
++ }
++ }
++ }
+ }
-+ queuedEdges.addAll(sections);
+ }
+
-+ tasks.schedule();
++ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List<Entity> into,
++ final Predicate<? super Entity> predicate, final int maxCount) {
++ if (this.count == 0) {
++ return false;
++ }
+
-+ return tasks;
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
++ if (part == except || !part.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ }
++ }
++ }
++ }
++
++ return false;
++ }
++
++ public void getEntitiesWithEnderDragonParts(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
++ final Predicate<? super Entity> predicate) {
++ if (this.count == 0) {
++ return;
++ }
++
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ }
++ }
++ }
++ }
++ }
++
++ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
++ final Predicate<? super Entity> predicate, final int maxCount) {
++ if (this.count == 0) {
++ return false;
++ }
++
++ final int minSection = this.slices.minSection;
++ final int maxSection = this.slices.maxSection;
++
++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++
++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ }
++ }
++ }
++ }
++
++ return false;
++ }
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3a8c192d1aed186ff506d69e3960e3b2792ddbd1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+@@ -0,0 +1,1044 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
+
-+ public void removeChunk(final ChunkPos pos) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos));
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
++import ca.spottedleaf.moonrise.common.list.EntityList;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.util.AbortableIterationConsumer;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.entity.EntityInLevelCallback;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.level.entity.LevelCallback;
++import net.minecraft.world.level.entity.LevelEntityGetter;
++import net.minecraft.world.level.entity.Visibility;
++import net.minecraft.world.phys.AABB;
++import net.minecraft.world.phys.Vec3;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.List;
++import java.util.NoSuchElementException;
++import java.util.Objects;
++import java.util.UUID;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.function.Consumer;
++import java.util.function.Predicate;
++
++public abstract class EntityLookup implements LevelEntityGetter<Entity> {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class);
++
++ protected static final int REGION_SHIFT = 5;
++ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
++ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
++
++ public final Level world;
++
++ protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
++
++ protected final int minSection; // inclusive
++ protected final int maxSection; // inclusive
++ protected final LevelCallback<Entity> worldCallback;
++
++ protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
++ protected final ConcurrentHashMap<UUID, Entity> entityByUUID = new ConcurrentHashMap<>();
++ protected final EntityList accessibleEntities = new EntityList();
++
++ public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
++ this.world = world;
++ this.minSection = WorldUtil.getMinSection(world);
++ this.maxSection = WorldUtil.getMaxSection(world);
++ this.worldCallback = worldCallback;
++ }
++
++ protected abstract Boolean blockTicketUpdates();
++
++ protected abstract void setBlockTicketUpdates(final Boolean value);
++
++ protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason);
++
++ protected abstract void checkThread(final Entity entity, final String reason);
++
++ protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk);
++
++ protected abstract void onEmptySlices(final int chunkX, final int chunkZ);
++
++ private static Entity maskNonAccessible(final Entity entity) {
++ if (entity == null) {
++ return null;
+ }
-+ if (tasks != null && tasks.cancel()) {
-+ tasks.onComplete.complete(null);
++ final Visibility visibility = EntityLookup.getEntityStatus(entity);
++ return visibility.isAccessible() ? entity : null;
++ }
++
++ @Override
++ public Entity get(final int id) {
++ return maskNonAccessible(this.entityById.get((long)id));
++ }
++
++ @Override
++ public Entity get(final UUID id) {
++ return maskNonAccessible(this.entityByUUID.get(id));
++ }
++
++ public boolean hasEntity(final UUID uuid) {
++ return this.get(uuid) != null;
++ }
++
++ public String getDebugInfo() {
++ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size();
++ }
++
++ protected static final class ArrayIterable<T> implements Iterable<T> {
++
++ private final T[] array;
++ private final int off;
++ private final int length;
++
++ public ArrayIterable(final T[] array, final int off, final int length) {
++ this.array = array;
++ this.off = off;
++ this.length = length;
++ if (length > array.length) {
++ throw new IllegalArgumentException("Length must be no greater-than the array length");
++ }
++ }
++
++ @Override
++ public Iterator<T> iterator() {
++ return new ArrayIterator<>(this.array, this.off, this.length);
++ }
++
++ protected static final class ArrayIterator<T> implements Iterator<T> {
++
++ private final T[] array;
++ private int off;
++ private final int length;
++
++ public ArrayIterator(final T[] array, final int off, final int length) {
++ this.array = array;
++ this.off = off;
++ this.length = length;
++ }
++
++ @Override
++ public boolean hasNext() {
++ return this.off < this.length;
++ }
++
++ @Override
++ public T next() {
++ if (this.off >= this.length) {
++ throw new NoSuchElementException();
++ }
++ return this.array[this.off++];
++ }
++
++ @Override
++ public void remove() {
++ throw new UnsupportedOperationException();
++ }
+ }
+ }
+
-+ public static final class ChunkTasks implements Runnable {
++ @Override
++ public Iterable<Entity> getAll() {
++ synchronized (this.accessibleEntities) {
++ final int len = this.accessibleEntities.size();
++ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class);
+
-+ public final CompletableFuture<Void> onComplete = new CompletableFuture<>();
-+ public boolean isTicketAdded;
-+ public final long chunkCoordinate;
++ Objects.checkFromToIndex(0, len, cpy.length);
+
-+ private final StarLightInterface lightEngine;
-+ private final LightQueue queue;
-+ private final PrioritisedExecutor.PrioritisedTask task;
-+ private final Set<BlockPos> changedPositions = new HashSet<>();
-+ private Boolean[] changedSectionSet;
-+ private ShortOpenHashSet queuedEdgeChecksSky;
-+ private ShortOpenHashSet queuedEdgeChecksBlock;
-+ private List<BooleanSupplier> lightTasks;
++ return new ArrayIterable<>(cpy, 0, len);
++ }
++ }
+
-+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) {
-+ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
++ public int getEntityCount() {
++ synchronized (this.accessibleEntities) {
++ return this.accessibleEntities.size();
+ }
++ }
+
-+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue,
-+ final PrioritisedExecutor.Priority priority) {
-+ this.chunkCoordinate = chunkCoordinate;
-+ this.lightEngine = lightEngine;
-+ this.queue = queue;
-+ this.task = queue.world.chunkTaskScheduler.radiusAwareScheduler.createTask(
-+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate),
-+ ChunkStatus.LIGHT.writeRadius, this, priority
-+ );
++ public Entity[] getAllCopy() {
++ synchronized (this.accessibleEntities) {
++ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
+ }
++ }
+
-+ public void schedule() {
-+ this.task.queue();
++ @Override
++ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) {
++ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) {
++ final Entity entity = iterator.next();
++ final Visibility visibility = EntityLookup.getEntityStatus(entity);
++ if (!visibility.isAccessible()) {
++ continue;
++ }
++ final U casted = filter.tryCast(entity);
++ if (casted != null && action.accept(casted).shouldAbort()) {
++ break;
++ }
+ }
++ }
+
-+ public boolean cancel() {
-+ return this.task.cancel();
++ @Override
++ public void get(final AABB box, final Consumer<Entity> action) {
++ List<Entity> entities = new ArrayList<>();
++ this.getEntitiesWithoutDragonParts(null, box, entities, null);
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ action.accept(entities.get(i));
+ }
++ }
+
-+ public PrioritisedExecutor.Priority getPriority() {
-+ return this.task.getPriority();
++ @Override
++ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) {
++ List<Entity> entities = new ArrayList<>();
++ this.getEntitiesWithoutDragonParts(null, box, entities, null);
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ final U casted = filter.tryCast(entities.get(i));
++ if (casted != null && action.accept(casted).shouldAbort()) {
++ break;
++ }
+ }
++ }
+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ this.task.lowerPriority(priority);
++ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved,
++ final boolean created, final boolean destroyed) {
++ this.checkThread(entity, "Entity status change must only happen on the main thread");
++
++ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
++ // recursive status update
++ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable());
++ return;
+ }
+
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ this.task.setPriority(priority);
++ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
++
++ if (entityStatusUpdateBefore) {
++ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
++ return;
+ }
+
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ this.task.raisePriority(priority);
++ try {
++ final Boolean ticketBlockBefore = this.blockTicketUpdates();
++ try {
++ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true);
++ try {
++ if (created) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onCreated(entity);
++ }
++ }
++
++ if (oldVisibility == newVisibility) {
++ if (moved && newVisibility.isAccessible()) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onSectionChange(entity);
++ }
++ }
++ return;
++ }
++
++ if (newVisibility.ordinal() > oldVisibility.ordinal()) {
++ // status upgrade
++ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
++ synchronized (this.accessibleEntities) {
++ this.accessibleEntities.add(entity);
++ }
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onTrackingStart(entity);
++ }
++ }
++
++ if (!oldVisibility.isTicking() && newVisibility.isTicking()) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onTickingStart(entity);
++ }
++ }
++ } else {
++ // status downgrade
++ if (oldVisibility.isTicking() && !newVisibility.isTicking()) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onTickingEnd(entity);
++ }
++ }
++
++ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
++ synchronized (this.accessibleEntities) {
++ this.accessibleEntities.remove(entity);
++ }
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onTrackingEnd(entity);
++ }
++ }
++ }
++
++ if (moved && newVisibility.isAccessible()) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onSectionChange(entity);
++ }
++ }
++
++ if (destroyed) {
++ if (EntityLookup.this.worldCallback != null) {
++ EntityLookup.this.worldCallback.onDestroyed(entity);
++ }
++ }
++ } finally {
++ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false);
++ }
++ } finally {
++ this.setBlockTicketUpdates(ticketBlockBefore);
++ }
++ } finally {
++ if (slices != null) {
++ slices.stopPreventingStatusUpdates(false);
++ }
+ }
++ }
+
-+ @Override
-+ public void run() {
-+ synchronized (this.queue) {
-+ this.queue.chunkTasks.remove(this.chunkCoordinate);
++ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
++ this.getChunk(x, z).updateStatus(newStatus, this);
++ }
++
++ public void addLegacyChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
++ this.addEntityChunk(entities, forChunk, true);
++ }
++
++ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
++ this.addEntityChunk(entities, forChunk, true);
++ }
++
++ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
++ this.addEntityChunk(entities, forChunk, false);
++ }
++
++ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
++ if (!this.addEntity(root, fromDisk)) {
++ // possible we are a passenger, and so should dismount from any valid entity in the world
++ root.stopRiding();
++ return;
++ }
++ for (final Entity passenger : root.getPassengers()) {
++ this.addRecursivelySafe(passenger, fromDisk);
++ }
++ }
++
++ protected void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) {
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ final Entity entity = entities.get(i);
++ if (entity.isPassenger()) {
++ continue;
+ }
+
-+ boolean litChunk = false;
-+ if (this.lightTasks != null) {
-+ for (final BooleanSupplier run : this.lightTasks) {
-+ if (run.getAsBoolean()) {
-+ litChunk = true;
-+ break;
++ if (forChunk != null && !entity.chunkPosition().equals(forChunk)) {
++ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk);
++ // can't set removed here, as we may not own the chunk position
++ // skip the entity
++ continue;
++ }
++
++ final Vec3 rootPosition = entity.position();
++
++ // always adjust positions before adding passengers in case plugins access the entity, and so that
++ // they are added to the right entity chunk
++ for (final Entity passenger : entity.getIndirectPassengers()) {
++ if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) {
++ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z);
++ }
++ }
++
++ this.addRecursivelySafe(entity, fromDisk);
++ }
++ }
++
++ public boolean addNewEntity(final Entity entity) {
++ return this.addEntity(entity, false);
++ }
++
++ public static Visibility getEntityStatus(final Entity entity) {
++ if (entity.isAlwaysTicking()) {
++ return Visibility.TICKING;
++ }
++ final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus();
++ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
++ }
++
++ protected boolean addEntity(final Entity entity, final boolean fromDisk) {
++ final BlockPos pos = entity.blockPosition();
++ final int sectionX = pos.getX() >> 4;
++ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
++ final int sectionZ = pos.getZ() >> 4;
++ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread");
++
++ if (entity.isRemoved()) {
++ LOGGER.warn("Refusing to add removed entity: " + entity);
++ return false;
++ }
++
++ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
++ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable());
++ return false;
++ }
++
++ Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity);
++ if (currentlyMapped != null) {
++ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity);
++ return false;
++ }
++
++ currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity);
++ if (currentlyMapped != null) {
++ // need to remove mapping for id
++ this.entityById.remove((long)entity.getId(), entity);
++ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity);
++ return false;
++ }
++
++ ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX);
++ ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY);
++ ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ);
++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
++ if (!slices.addEntity(entity, sectionY)) {
++ LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")");
++ }
++
++ entity.setLevelCallback(new EntityCallback(entity));
++
++ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
++
++ return true;
++ }
++
++ public boolean canRemoveEntity(final Entity entity) {
++ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
++ return false;
++ }
++
++ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
++ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
++ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
++ return slices == null || !slices.isPreventingStatusUpdates();
++ }
++
++ protected void removeEntity(final Entity entity) {
++ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
++ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
++ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
++ this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main");
++ if (!entity.isRemoved()) {
++ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity");
++ }
++ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
++ // all entities should be in a chunk
++ if (slices == null) {
++ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")");
++ } else {
++ if (slices.isPreventingStatusUpdates()) {
++ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates");
++ }
++ if (!slices.removeEntity(entity, sectionY)) {
++ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
++ }
++ }
++ ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE);
++ ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE);
++ ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE);
++
++
++ Entity currentlyMapped;
++ if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) {
++ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped);
++ }
++
++ Entity[] currentlyMappedArr = new Entity[1];
++
++ // need reference equality
++ this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> {
++ currentlyMappedArr[0] = valueInMap;
++ if (valueInMap != entity) {
++ return valueInMap;
++ }
++ return null;
++ });
++
++ if (currentlyMappedArr[0] != entity) {
++ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]);
++ }
++
++ if (slices != null && slices.isEmpty()) {
++ this.onEmptySlices(sectionX, sectionZ);
++ }
++ }
++
++ protected ChunkEntitySlices moveEntity(final Entity entity) {
++ // ensure we own the entity
++ this.checkThread(entity, "Cannot move entity off-main");
++
++ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
++ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
++ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
++ final BlockPos newPos = entity.blockPosition();
++ final int newSectionX = newPos.getX() >> 4;
++ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
++ final int newSectionZ = newPos.getZ() >> 4;
++
++ if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) {
++ return null;
++ }
++
++ // ensure the new section is owned by this tick thread
++ this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main");
++
++ // ensure the old section is owned by this tick thread
++ this.checkThread(sectionX, sectionZ, "Cannot move entity off-main");
++
++ final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ);
++ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
++
++ if (!old.removeEntity(entity, sectionY)) {
++ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section");
++ }
++
++ if (!slices.addEntity(entity, newSectionY)) {
++ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section");
++ }
++
++ ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX);
++ ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY);
++ ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ);
++
++ if (old.isEmpty()) {
++ this.onEmptySlices(sectionX, sectionZ);
++ }
++
++ return slices;
++ }
++
++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate);
+ }
+ }
+ }
++ }
++ }
+
-+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine();
-+ try {
-+ final long coordinate = this.chunkCoordinate;
-+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
-+ final Set<BlockPos> positions = this.changedPositions;
-+ final Boolean[] sectionChanges = this.changedSectionSet;
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
-+ if (!litChunk) {
-+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ chunk.getEntities(except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ chunk.getHardCollidingEntities(except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
+ }
-+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
++ }
++ }
++ }
++ }
++
++ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ chunk.getEntities(clazz, except, box, into, predicate);
+ }
++ }
++ }
++ }
++ }
++
++ //////// Limited ////////
++
++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
++ final int maxCount) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
+
-+ if (skyEngine != null && this.queuedEdgeChecksSky != null) {
-+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky);
++ if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) {
++ return;
++ }
+ }
-+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) {
-+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock);
++ }
++ }
++ }
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
++ final int maxCount) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ if (chunk.getEntities(except, box, into, predicate, maxCount)) {
++ return;
++ }
+ }
+ }
++ }
++ }
++ }
+
-+ this.onComplete.complete(null);
-+ } finally {
-+ this.lightEngine.releaseSkyLightEngine(skyEngine);
-+ this.lightEngine.releaseBlockLightEngine(blockEngine);
++ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate, final int maxCount) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) {
++ return;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
++ final Predicate<? super T> predicate, final int maxCount) {
++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
++
++ final int minRegionX = minChunkX >> REGION_SHIFT;
++ final int minRegionZ = minChunkZ >> REGION_SHIFT;
++ final int maxRegionX = maxChunkX >> REGION_SHIFT;
++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
++
++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
++
++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
++
++ if (region == null) {
++ continue;
++ }
++
++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
++
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
++ continue;
++ }
++
++ if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) {
++ return;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
++ this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main");
++ synchronized (this) {
++ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ);
++ if (curr != null) {
++ this.removeChunk(chunkX, chunkZ);
++
++ curr.mergeInto(slices);
++
++ this.addChunk(chunkX, chunkZ, slices);
++ } else {
++ this.addChunk(chunkX, chunkZ, slices);
++ }
++ }
++ }
++
++ public void entitySectionUnload(final int chunkX, final int chunkZ) {
++ this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main");
++ this.removeChunk(chunkX, chunkZ);
++ }
++
++ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ if (region == null) {
++ return null;
++ }
++
++ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
++ }
++
++ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ ChunkEntitySlices ret;
++ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
++ return this.createEntityChunk(chunkX, chunkZ, true);
++ }
++
++ return ret;
++ }
++
++ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
++ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
++
++ return this.regions.get(key);
++ }
++
++ protected synchronized void removeChunk(final int chunkX, final int chunkZ) {
++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++
++ final ChunkSlicesRegion region = this.regions.get(key);
++ final int remaining = region.remove(relIndex);
++
++ if (remaining == 0) {
++ this.regions.remove(key);
++ }
++ }
++
++ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
++
++ ChunkSlicesRegion region = this.regions.get(key);
++ if (region != null) {
++ region.add(relIndex, slices);
++ } else {
++ region = new ChunkSlicesRegion();
++ region.add(relIndex, slices);
++ this.regions.put(key, region);
++ }
++ }
++
++ public static final class ChunkSlicesRegion {
++
++ private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
++ private int sliceCount;
++
++ public ChunkEntitySlices get(final int index) {
++ return this.slices[index];
++ }
++
++ public int remove(final int index) {
++ final ChunkEntitySlices slices = this.slices[index];
++ if (slices == null) {
++ throw new IllegalStateException();
++ }
++
++ this.slices[index] = null;
++
++ return --this.sliceCount;
++ }
++
++ public void add(final int index, final ChunkEntitySlices slices) {
++ final ChunkEntitySlices curr = this.slices[index];
++ if (curr != null) {
++ throw new IllegalStateException();
++ }
++
++ this.slices[index] = slices;
++
++ ++this.sliceCount;
++ }
++ }
++
++ protected final class EntityCallback implements EntityInLevelCallback {
++
++ public final Entity entity;
++
++ public EntityCallback(final Entity entity) {
++ this.entity = entity;
++ }
++
++ @Override
++ public void onMove() {
++ final Entity entity = this.entity;
++ final Visibility oldVisibility = getEntityStatus(entity);
++ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
++ if (newSlices == null) {
++ // no new section, so didn't change sections
++ return;
+ }
++ final Visibility newVisibility = getEntityStatus(entity);
++
++ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
++ }
++
++ @Override
++ public void onRemove(final Entity.RemovalReason reason) {
++ final Entity entity = this.entity;
++ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system
++ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
++
++ EntityLookup.this.removeEntity(entity);
++
++ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
++
++ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
+ }
+ }
++
++ protected static final class NoOpCallback implements EntityInLevelCallback {
++
++ public static final NoOpCallback INSTANCE = new NoOpCallback();
++
++ @Override
++ public void onMove() {}
++
++ @Override
++ public void onRemove(final Entity.RemovalReason reason) {}
++ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
new file mode 100644
-index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a91d426d47
+index 0000000000000000000000000000000000000000..fc4ea13aa4a21bd3d3f9377418a24b904868c401
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java
-@@ -0,0 +1,213 @@
-+package io.papermc.paper.chunk.system.poi;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
+@@ -0,0 +1,81 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.entity.LevelCallback;
+
-+import com.mojang.logging.LogUtils;
++public final class ClientEntityLookup extends EntityLookup {
++
++ private final LongOpenHashSet tickingChunks = new LongOpenHashSet();
++
++ public ClientEntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
++ super(world, worldCallback);
++ }
++
++ @Override
++ protected Boolean blockTicketUpdates() {
++ // not present on client
++ return null;
++ }
++
++ @Override
++ protected void setBlockTicketUpdates(Boolean value) {
++ // not present on client
++ }
++
++ @Override
++ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
++ // TODO implement?
++ }
++
++ @Override
++ protected void checkThread(final Entity entity, final String reason) {
++ // TODO implement?
++ }
++
++ @Override
++ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
++ final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++ final ChunkEntitySlices ret = new ChunkEntitySlices(
++ this.world, chunkX, chunkZ,
++ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
++ );
++
++ // note: not handled by superclass
++ this.addChunk(chunkX, chunkZ, ret);
++
++ return ret;
++ }
++
++ @Override
++ protected void onEmptySlices(final int chunkX, final int chunkZ) {
++ this.removeChunk(chunkX, chunkZ);
++ }
++
++ public void markTicking(final long pos) {
++ if (this.tickingChunks.add(pos)) {
++ final int chunkX = CoordinateUtils.getChunkX(pos);
++ final int chunkZ = CoordinateUtils.getChunkZ(pos);
++ if (this.getChunk(chunkX, chunkZ) != null) {
++ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING);
++ }
++ }
++ }
++
++ public void markNonTicking(final long pos) {
++ if (this.tickingChunks.remove(pos)) {
++ final int chunkX = CoordinateUtils.getChunkX(pos);
++ final int chunkZ = CoordinateUtils.getChunkZ(pos);
++ if (this.getChunk(chunkX, chunkZ) != null) {
++ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL);
++ }
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a9b0e8e90f433e141f36e47a9331cbdcb9ac9817
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
+@@ -0,0 +1,72 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.entity.LevelCallback;
++
++public final class DefaultEntityLookup extends EntityLookup {
++ public DefaultEntityLookup(final Level world) {
++ super(world, new DefaultLevelCallback());
++ }
++
++ @Override
++ protected Boolean blockTicketUpdates() {
++ return null;
++ }
++
++ @Override
++ protected void setBlockTicketUpdates(final Boolean value) {}
++
++ @Override
++ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {}
++
++ @Override
++ protected void checkThread(final Entity entity, final String reason) {}
++
++ @Override
++ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
++ final ChunkEntitySlices ret = new ChunkEntitySlices(
++ this.world, chunkX, chunkZ, FullChunkStatus.FULL,
++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
++ );
++
++ // note: not handled by superclass
++ this.addChunk(chunkX, chunkZ, ret);
++
++ return ret;
++ }
++
++ @Override
++ protected void onEmptySlices(final int chunkX, final int chunkZ) {
++ this.removeChunk(chunkX, chunkZ);
++ }
++
++ protected static final class DefaultLevelCallback implements LevelCallback<Entity> {
++
++ @Override
++ public void onCreated(final Entity entity) {}
++
++ @Override
++ public void onDestroyed(final Entity entity) {}
++
++ @Override
++ public void onTickingStart(final Entity entity) {}
++
++ @Override
++ public void onTickingEnd(final Entity entity) {}
++
++ @Override
++ public void onTrackingStart(final Entity entity) {}
++
++ @Override
++ public void onTrackingEnd(final Entity entity) {}
++
++ @Override
++ public void onSectionChange(final Entity entity) {}
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5b68279cae5952bdb7bdef3668980385a3a643e0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+@@ -0,0 +1,50 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.entity.LevelCallback;
++
++public final class ServerEntityLookup extends EntityLookup {
++
++ private final ServerLevel serverWorld;
++
++ public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
++ super(world, worldCallback);
++ this.serverWorld = world;
++ }
++
++ @Override
++ protected Boolean blockTicketUpdates() {
++ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates();
++ }
++
++ @Override
++ protected void setBlockTicketUpdates(final Boolean value) {
++ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value);
++ }
++
++ @Override
++ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
++ }
++
++ @Override
++ protected void checkThread(final Entity entity, final String reason) {
++ io.papermc.paper.util.TickThread.ensureTickThread(entity, reason);
++ }
++
++ @Override
++ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
++ // loadInEntityChunk will call addChunk for us
++ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager
++ .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk);
++ }
++
++ @Override
++ protected void onEmptySlices(final int chunkX, final int chunkZ) {
++ // entity slices unloading is managed by ticket levels in chunk system
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
+@@ -0,0 +1,17 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.chunk.ChunkAccess;
++
++public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage {
++
++ public ServerLevel moonrise$getWorld();
++
++ public void moonrise$onUnload(final long coordinate);
++
++ public void moonrise$loadInPoiChunk(final PoiChunk poiChunk);
++
++ public void moonrise$checkConsistency(final ChunkAccess chunk);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
+@@ -0,0 +1,12 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
++
++import net.minecraft.world.entity.ai.village.poi.PoiSection;
++import java.util.Optional;
++
++public interface ChunkSystemPoiSection {
++
++ public boolean moonrise$isEmpty();
++
++ public Optional<PoiSection> moonrise$asOptional();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cd1302a3aee6f543f39d71b91725128fa1aeddcc
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
+@@ -0,0 +1,211 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import io.papermc.paper.util.WorldUtil;
+import net.minecraft.SharedConstants;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
@@ -5124,12 +7172,12 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+import net.minecraft.world.entity.ai.village.poi.PoiManager;
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import org.slf4j.Logger;
-+
++import org.slf4j.LoggerFactory;
+import java.util.Optional;
+
+public final class PoiChunk {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
@@ -5137,7 +7185,7 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ public final int minSection;
+ public final int maxSection;
+
-+ protected final PoiSection[] sections;
++ private final PoiSection[] sections;
+
+ private boolean isDirty;
+ private boolean loaded;
@@ -5159,12 +7207,12 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ }
+
+ public void load() {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
+ if (this.loaded) {
+ return;
+ }
+ this.loaded = true;
-+ this.world.chunkSource.getPoiManager().loadInPoiChunk(this);
++ ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this);
+ }
+
+ public boolean isLoaded() {
@@ -5173,7 +7221,7 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+
+ public boolean isEmpty() {
+ for (final PoiSection section : this.sections) {
-+ if (section != null && !section.isEmpty()) {
++ if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ return false;
+ }
+ }
@@ -5209,7 +7257,7 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ public Optional<PoiSection> getSectionForVanilla(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ final PoiSection ret = this.sections[chunkY - this.minSection];
-+ return ret == null ? Optional.empty() : ret.noAllocateOptional;
++ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional();
+ }
+ return Optional.empty();
+ }
@@ -5224,7 +7272,7 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+
+ // returns null if empty
+ public CompoundTag save() {
-+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess);
++ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess());
+
+ final CompoundTag ret = new CompoundTag();
+ final CompoundTag sections = new CompoundTag();
@@ -5238,8 +7286,8 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ final int chunkZ = this.chunkZ;
+
+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
-+ final PoiSection chunk = this.sections[sectionY - this.minSection];
-+ if (chunk == null || chunk.isEmpty()) {
++ final PoiSection section = this.sections[sectionY - this.minSection];
++ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ continue;
+ }
+
@@ -5249,10 +7297,10 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ poiManager.setDirty(key);
+ });
+
-+ final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, chunk);
++ final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, section);
+ final int finalSectionY = sectionY;
+ final Tag serialized = serializedResult.resultOrPartial((final String description) -> {
-+ LOGGER.error("Failed to serialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
++ LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
+ }).orElse(null);
+ if (serialized == null) {
+ // failed, should be logged from the resultOrPartial
@@ -5274,7 +7322,7 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) {
+ final PoiChunk ret = empty(world, chunkX, chunkZ);
+
-+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess);
++ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess());
+
+ final CompoundTag sections = data.getCompound("Sections");
+
@@ -5303,10 +7351,10 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ final DataResult<PoiSection> deserializeResult = codec.parse(registryOps, section);
+ final int finalSectionY = sectionY;
+ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> {
-+ LOGGER.error("Failed to deserialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
++ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
+ }).orElse(null);
+
-+ if (deserialized == null || deserialized.isEmpty()) {
++ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) {
+ // completely empty, no point in storing this
+ continue;
+ }
@@ -5320,167 +7368,1295 @@ index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a9
+ return ret;
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3f5edb756beb9c31b6f591a24b778d6ac2b0bf51
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
+@@ -0,0 +1,21 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
++
++import com.mojang.serialization.Dynamic;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.world.level.chunk.storage.RegionFileStorage;
++import java.io.IOException;
++import java.util.Optional;
++import java.util.concurrent.CompletableFuture;
++
++public interface ChunkSystemSectionStorage {
++
++ public CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws IOException;
++
++ public void moonrise$write(final int chunkX, final int chunkZ, final CompoundTag data) throws IOException;
++
++ public RegionFileStorage moonrise$getRegionStorage();
++
++ public void moonrise$close() throws IOException;
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c307b084f59f7bb94dc02f25bbcd3e01e01d2306
+index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java
-@@ -0,0 +1,131 @@
-+package io.papermc.paper.chunk.system.scheduling;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
+@@ -0,0 +1,15 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.player;
++
++public interface ChunkSystemServerPlayer {
++
++ public boolean moonrise$isRealPlayer();
++
++ public void moonrise$setRealPlayer(final boolean real);
++
++ public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader();
++
++ public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader);
++
++ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..dba09cb32844533c383635e7623f5180a468f636
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+@@ -0,0 +1,1059 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.poi.PoiChunk;
-+import net.minecraft.server.level.ChunkMap;
++import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter;
++import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayList;
++import it.unimi.dsi.fastutil.longs.LongComparator;
++import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import net.minecraft.network.protocol.Packet;
++import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
++import net.minecraft.server.level.ChunkTrackingView;
+import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.server.network.PlayerChunkSender;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import org.slf4j.Logger;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
+import java.lang.invoke.VarHandle;
++import java.util.ArrayDeque;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.function.Function;
+
-+public final class ChunkFullTask extends ChunkProgressionTask implements Runnable {
++public final class RegionizedPlayerChunkLoader {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
++ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
+
-+ protected final NewChunkHolder chunkHolder;
-+ protected final ChunkAccess fromChunk;
-+ protected final PrioritisedExecutor.PrioritisedTask convertToFullTask;
++ public static final int MIN_VIEW_DISTANCE = 2;
++ public static final int MAX_VIEW_DISTANCE = 32;
+
-+ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ this.chunkHolder = chunkHolder;
-+ this.fromChunk = fromChunk;
-+ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority);
++ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL;
++ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
++ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
++
++ public static class ViewDistanceHolder {
++
++ private volatile ViewDistances viewDistances;
++ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class);
++
++ public ViewDistanceHolder() {
++ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1));
++ }
++
++ public ViewDistances getViewDistances() {
++ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this);
++ }
++
++ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) {
++ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update);
++ }
++
++ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) {
++ int failures = 0;
++ for (ViewDistances curr = this.getViewDistances();;) {
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) {
++ return;
++ }
++ ++failures;
++ }
++ }
++
++ public void setTickViewDistance(final int distance) {
++ this.updateViewDistance((final ViewDistances param) -> {
++ return param.setTickViewDistance(distance);
++ });
++ }
++
++ public void setLoadViewDistance(final int distance) {
++ this.updateViewDistance((final ViewDistances param) -> {
++ return param.setLoadViewDistance(distance);
++ });
++ }
++
++ public void setSendViewDistance(final int distance) {
++ this.updateViewDistance((final ViewDistances param) -> {
++ return param.setTickViewDistance(distance);
++ });
++ }
+ }
+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.FULL;
++ public static final record ViewDistances(
++ int tickViewDistance,
++ int loadViewDistance,
++ int sendViewDistance
++ ) {
++ public ViewDistances setTickViewDistance(final int distance) {
++ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
++ }
++
++ public ViewDistances setLoadViewDistance(final int distance) {
++ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
++ }
++
++ public ViewDistances setSendViewDistance(final int distance) {
++ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
++ }
+ }
+
-+ @Override
-+ public void run() {
-+ // See Vanilla protoChunkToFullChunk for what this function should be doing
-+ final LevelChunk chunk;
-+ try {
-+ // moved from the load from nbt stage into here
-+ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk();
-+ if (poiChunk == null) {
-+ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
-+ } else {
-+ poiChunk.load();
-+ this.world.getPoiManager().checkConsistency(this.fromChunk);
-+ }
++ public static int getAPITickViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (data == null) {
++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance();
++ }
++ return data.lastTickDistance;
++ }
+
-+ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) {
-+ chunk = wrappedFull.getWrapped();
-+ } else {
-+ final ServerLevel world = this.world;
-+ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
-+ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
-+ ChunkMap.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - rewrite chunk system
-+ });
-+ }
++ public static int getAPIViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (data == null) {
++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
++ }
++ // view distance = load distance + 1
++ return data.lastLoadDistance - 1;
++ }
+
-+ chunk.setChunkHolder(this.scheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ)); // replaces setFullStatus
-+ chunk.runPostLoad();
-+ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla)
-+ // This brings entity addition back in line with older versions of the game
-+ // Since we load the NBT in the empty status, this will never block for I/O
-+ this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false);
++ public static int getLoadViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (data == null) {
++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
++ }
++ // view distance = load distance + 1
++ return data.lastLoadDistance - 1;
++ }
+
-+ // we don't need the entitiesInLevel trash, this system doesn't double run callbacks
-+ chunk.setLoaded(true);
-+ chunk.registerAllBlockEntitiesAfterLevelLoad();
-+ chunk.registerTickContainerInLevel(this.world);
-+ } catch (final Throwable throwable) {
-+ this.complete(null, throwable);
++ public static int getAPISendViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (data == null) {
++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance();
++ }
++ return data.lastSendDistance;
++ }
++
++ private final ServerLevel world;
++
++ public RegionizedPlayerChunkLoader(final ServerLevel world) {
++ this.world = world;
++ }
++
++ public void addPlayer(final ServerPlayer player) {
++ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
++ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
+ return;
+ }
-+ this.complete(chunk, null);
++
++ if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) {
++ throw new IllegalStateException("Player is already added to player chunk loader");
++ }
++
++ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
++
++ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader);
++ loader.add();
+ }
+
-+ protected volatile boolean scheduled;
-+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class);
++ public void updatePlayer(final ServerPlayer player) {
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (loader != null) {
++ loader.update();
++ }
++ }
+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
++ public void removePlayer(final ServerPlayer player) {
++ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
++ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
++ return;
++ }
++
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++
++ if (loader == null) {
++ return;
++ }
++
++ loader.remove();
++ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null);
+ }
+
-+ @Override
-+ public void schedule() {
-+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) {
-+ throw new IllegalStateException("Cannot double call schedule()");
++ public void setSendDistance(final int distance) {
++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance);
++ }
++
++ public void setLoadDistance(final int distance) {
++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance);
++ }
++
++ public void setTickDistance(final int distance) {
++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance);
++ }
++
++ // Note: follow the player chunk loader so everything stays consistent...
++ public int getAPITickDistance() {
++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
++ -1, distances.tickViewDistance,
++ -1, distances.loadViewDistance
++ );
++ return tickViewDistance;
++ }
++
++ public int getAPIViewDistance() {
++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
++ -1, distances.tickViewDistance,
++ -1, distances.loadViewDistance
++ );
++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
++
++ // loadDistance = api view distance + 1
++ return loadDistance - 1;
++ }
++
++ public int getAPISendViewDistance() {
++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
++ -1, distances.tickViewDistance,
++ -1, distances.loadViewDistance
++ );
++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
++ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
++ loadDistance, -1, -1, distances.sendViewDistance
++ );
++
++ return sendViewDistance;
++ }
++
++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
++ }
++
++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (loader == null) {
++ return false;
+ }
-+ this.convertToFullTask.queue();
++
++ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
-+ @Override
-+ public void cancel() {
-+ if (this.convertToFullTask.cancel()) {
-+ this.complete(null, null);
++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (loader == null) {
++ return false;
++ }
++
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
++ return true;
++ }
++ }
+ }
++
++ return false;
+ }
+
-+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
-+ return this.convertToFullTask.getPriority();
++ public void tick() {
++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick player chunk loader async");
++ long currTime = System.nanoTime();
++ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (loader == null || loader.removed || loader.world != this.world) {
++ // not our problem anymore
++ continue;
++ }
++ loader.update(); // can't invoke plugin logic
++ loader.updateQueues(currTime);
++ }
+ }
+
-+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
++ public static final class PlayerChunkLoaderData {
++
++ private static final AtomicLong ID_GENERATOR = new AtomicLong();
++ private final long id = ID_GENERATOR.incrementAndGet();
++ private final Long idBoxed = Long.valueOf(this.id);
++
++ private static final long MAX_RATE = 10_000L;
++
++ private final ServerPlayer player;
++ private final ServerLevel world;
++
++ private int lastChunkX = Integer.MIN_VALUE;
++ private int lastChunkZ = Integer.MIN_VALUE;
++
++ private int lastSendDistance = Integer.MIN_VALUE;
++ private int lastLoadDistance = Integer.MIN_VALUE;
++ private int lastTickDistance = Integer.MIN_VALUE;
++
++ private int lastSentChunkCenterX = Integer.MIN_VALUE;
++ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
++
++ private int lastSentChunkRadius = Integer.MIN_VALUE;
++ private int lastSentSimulationDistance = Integer.MIN_VALUE;
++
++ private boolean canGenerateChunks = true;
++
++ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
++ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
++
++ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
++ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
++ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
++ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
++ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
++ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
++ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
++ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
++ LOADED_TICKET_LEVEL,
++ LOADED_TICKET_LEVEL,
++ GENERATED_TICKET_LEVEL,
++ GENERATED_TICKET_LEVEL,
++ TICK_TICKET_LEVEL
++ };
++ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
++ {
++ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
+ }
-+ this.convertToFullTask.lowerPriority(priority);
++
++ // rate limiting
++ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
++ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
++ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
++ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
++
++ // queues
++ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
++ final int c1x = CoordinateUtils.getChunkX(c1);
++ final int c1z = CoordinateUtils.getChunkZ(c1);
++
++ final int c2x = CoordinateUtils.getChunkX(c2);
++ final int c2z = CoordinateUtils.getChunkZ(c2);
++
++ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
++ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
++
++ return Integer.compare(
++ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
++ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
++ );
++ };
++ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
++
++ private volatile boolean removed;
++
++ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
++ this.world = world;
++ this.player = player;
++ }
++
++ private void flushDelayedTicketOps() {
++ if (this.delayedTicketOps.isEmpty()) {
++ return;
++ }
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps);
++ this.delayedTicketOps.clear();
++ }
++
++ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
++ this.delayedTicketOps.addLast(op);
++ }
++
++ private void sendChunk(final int chunkX, final int chunkZ) {
++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player);
++ PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ));
++ return;
++ }
++ throw new IllegalStateException();
++ }
++
++ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
++ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ return;
++ }
++ this.sendUnloadChunkRaw(chunkX, chunkZ);
++ }
++
++ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
++ // Note: Check PlayerChunkSender#dropChunk for other logic
++ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies
++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player);
++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ)));
++ }
++
++ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
++ @Override
++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ // do nothing, we only care about remove
++ }
++
++ @Override
++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ parameter.sendUnloadChunk(chunkX, chunkZ);
++ }
++ };
++ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
++ @Override
++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ // do nothing, we only care about remove
++ }
++
++ @Override
++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
++ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
++ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
++ return;
++ }
++
++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
++ chunk,
++ PLAYER_TICKET_DELAYED, level, parameter.idBoxed,
++ PLAYER_TICKET, level, parameter.idBoxed
++ ));
++ }
++ };
++ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
++ @Override
++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ // do nothing, we will detect ticking chunks when we try to load them
++ }
++
++ @Override
++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
++ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
++ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
++ return;
++ }
++
++ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that
++ // the level is kept for a short period of time
++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
++ chunk,
++ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed,
++ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
++ ));
++ // keep chunk at new generated level
++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
++ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
++ ));
++ }
++ };
++
++ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
++ final int sendRadius) {
++ // expect sendRadius to be = 1 + target viewable radius
++ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true);
++ }
++
++ private static int getClientViewDistance(final ServerPlayer player) {
++ final Integer vd = player.requestedViewDistance();
++ return vd == null ? -1 : Math.max(0, vd.intValue());
++ }
++
++ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance,
++ final int playerLoadViewDistance, final int worldLoadViewDistance) {
++ return Math.min(
++ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance,
++ playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance
++ );
++ }
++
++ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
++ final int worldLoadViewDistance) {
++ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
++ }
++
++ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
++ final int playerSendViewDistance, final int worldSendViewDistance) {
++ return Math.min(
++ loadViewDistance - 1,
++ playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
++ );
++ }
++
++ private Packet<?> updateClientChunkRadius(final int radius) {
++ this.lastSentChunkRadius = radius;
++ return new ClientboundSetChunkCacheRadiusPacket(radius);
++ }
++
++ private Packet<?> updateClientSimulationDistance(final int distance) {
++ this.lastSentSimulationDistance = distance;
++ return new ClientboundSetSimulationDistancePacket(distance);
++ }
++
++ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
++ this.lastSentChunkCenterX = chunkX;
++ this.lastSentChunkCenterZ = chunkZ;
++ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
++ }
++
++ private boolean canPlayerGenerateChunks() {
++ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
++ }
++
++ private double getMaxChunkLoadRate() {
++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
++
++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
++ }
++
++ private double getMaxChunkGenRate() {
++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
++
++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
++ }
++
++ private double getMaxChunkSendRate() {
++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
++
++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
++ }
++
++ private long getMaxChunkLoads() {
++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
++ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
++ if (configLimit == 0L) {
++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
++ configLimit = Math.max(5L, radiusChunks / 5L);
++ } else if (configLimit < 0L) {
++ configLimit = Integer.MAX_VALUE;
++ } // else: use the value configured
++ configLimit = configLimit - this.loadingQueue.size();
++
++ return configLimit;
++ }
++
++ private long getMaxChunkGenerates() {
++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
++ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
++ if (configLimit == 0L) {
++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
++ configLimit = Math.max(5L, radiusChunks / 5L);
++ } else if (configLimit < 0L) {
++ configLimit = Integer.MAX_VALUE;
++ } // else: use the value configured
++ configLimit = configLimit - this.generatingQueue.size();
++
++ return configLimit;
++ }
++
++ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
++ final int dx = this.lastChunkX - chunkX;
++ final int dz = this.lastChunkZ - chunkZ;
++ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded(
++ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
++ );
++ }
++
++ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
++ final int dx = this.lastChunkX - chunkX;
++ final int dz = this.lastChunkZ - chunkZ;
++ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
++ }
++
++ private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) {
++ for (int dz = -radius; dz <= radius; ++dz) {
++ for (int dx = -radius; dx <= radius; ++dx) {
++ if ((dx | dz) == 0) {
++ continue;
++ }
++
++ final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ);
++ final byte stage = this.chunkTicketStage.get(neighbour);
++
++ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ void updateQueues(final long time) {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
++ if (this.removed) {
++ throw new IllegalStateException("Ticking removed player chunk loader");
++ }
++ // update rate limits
++ final double loadRate = this.getMaxChunkLoadRate();
++ final double genRate = this.getMaxChunkGenRate();
++ final double sendRate = this.getMaxChunkSendRate();
++
++ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
++ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
++ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
++
++ // try to progress chunk loads
++ while (!this.loadingQueue.isEmpty()) {
++ final long pendingLoadChunk = this.loadingQueue.firstLong();
++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
++ final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ);
++ if (pending == null) {
++ // nothing to do here
++ break;
++ }
++ // chunk has loaded, so we can take it out of the queue
++ this.loadingQueue.dequeueLong();
++
++ // try to move to generate queue
++ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
++ if (prev != CHUNK_TICKET_STAGE_LOADING) {
++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
++ }
++
++ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
++ this.genQueue.enqueue(pendingLoadChunk);
++ } // else: don't want to generate, so just leave it loaded
++ }
++
++ // try to push more chunk loads
++ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
++ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
++ if (maxLoadsThisTick > 0) {
++ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
++ for (int i = 0; i < maxLoadsThisTick; ++i) {
++ final long chunk = this.loadQueue.dequeueLong();
++ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
++ if (prev != CHUNK_TICKET_STAGE_NONE) {
++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
++ }
++ this.pushDelayedTicketOp(
++ ChunkHolderManager.TicketOperation.addOp(
++ chunk,
++ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
++ )
++ );
++ chunks.add(chunk);
++ this.loadingQueue.enqueue(chunk);
++ }
++
++ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
++ this.flushDelayedTicketOps();
++ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
++ // load - only generate ticket levels start anything, but they start generation...
++ // propagate levels
++ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
++
++ if (this.removed) {
++ // process ticket updates may invoke plugin logic, which may remove this player
++ return;
++ }
++
++ for (int i = 0; i < maxLoadsThisTick; ++i) {
++ final long queuedLoadChunk = chunks.getLong(i);
++ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
++ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
++ );
++ if (this.removed) {
++ return;
++ }
++ }
++ }
++
++ // try to progress chunk generations
++ while (!this.generatingQueue.isEmpty()) {
++ final long pendingGenChunk = this.generatingQueue.firstLong();
++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
++ final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ);
++ if (pending == null) {
++ // nothing to do here
++ break;
++ }
++
++ // chunk has generated, so we can take it out of queue
++ this.generatingQueue.dequeueLong();
++
++ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
++ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
++ }
++
++ // try to move to send queue
++ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
++ this.sendQueue.enqueue(pendingGenChunk);
++ }
++ // try to move to tick queue
++ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
++ this.tickingQueue.enqueue(pendingGenChunk);
++ }
++ }
++
++ // try to push more chunk generations
++ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
++ // preview the allocations, as we may not actually utilise all of them
++ final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens);
++ long ratedGensThisTick = 0L;
++ while (!this.genQueue.isEmpty()) {
++ final long chunkKey = this.genQueue.firstLong();
++ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
++ final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
++ if (chunk.getPersistedStatus() != ChunkStatus.FULL) {
++ // only rate limit actual generations
++ if ((ratedGensThisTick + 1L) > maxGensThisTick) {
++ break;
++ }
++ ++ratedGensThisTick;
++ }
++
++ this.genQueue.dequeueLong();
++
++ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING);
++ if (prev != CHUNK_TICKET_STAGE_LOADED) {
++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
++ }
++ this.pushDelayedTicketOp(
++ ChunkHolderManager.TicketOperation.addAndRemove(
++ chunkKey,
++ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
++ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
++ )
++ );
++ this.generatingQueue.enqueue(chunkKey);
++ }
++ // take the allocations we actually used
++ this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick);
++
++ // try to pull ticking chunks
++ while (!this.tickingQueue.isEmpty()) {
++ final long pendingTicking = this.tickingQueue.firstLong();
++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
++
++ if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ,
++ ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) {
++ break;
++ }
++
++ // only gets here if all neighbours were marked as generated or ticking themselves
++ this.tickingQueue.dequeueLong();
++ this.pushDelayedTicketOp(
++ ChunkHolderManager.TicketOperation.addAndRemove(
++ pendingTicking,
++ PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
++ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
++ )
++ );
++ // note: there is no queue to add after ticking
++ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
++ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
++ }
++ }
++
++ // try to pull sending chunks
++ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends
++ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
++ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
++ for (int i = 0; i < maxSendsThisTick; ++i) {
++ final long pendingSend = this.sendQueue.firstLong();
++ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
++ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ);
++ if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
++ // nothing to do
++ // the target chunk may not be owned by this region, but this should be resolved in the future
++ break;
++ }
++ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
++ // not yet post-processed, need to do this so that tile entities can properly be sent to clients
++ chunk.postProcessGeneration();
++ // check if there was any recursive action
++ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
++ return;
++ } // else: good to dequeue and send, fall through
++ }
++ this.sendQueue.dequeueLong();
++
++ this.sendChunk(pendingSendX, pendingSendZ);
++
++ if (this.removed) {
++ // sendChunk may invoke plugin logic
++ return;
++ }
++ }
++
++ this.flushDelayedTicketOps();
++ }
++
++ void add() {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
++ if (this.removed) {
++ throw new IllegalStateException("Adding removed player chunk loader");
++ }
++ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
++ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
++ final int chunkX = this.player.chunkPosition().x;
++ final int chunkZ = this.player.chunkPosition().z;
++
++ final int tickViewDistance = getTickDistance(
++ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
++ playerDistances.loadViewDistance, worldDistances.loadViewDistance
++ );
++ // load view cannot be less-than tick view + 1
++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
++ // send view cannot be greater-than load view
++ final int clientViewDistance = getClientViewDistance(this.player);
++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
++
++ // TODO check PlayerList diff in paper chunk system patch
++ // send view distances
++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
++
++ // add to distance maps
++ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1);
++ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
++ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
++
++ // update chunk center
++ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
++
++ // reset limiters, they will start at a zero allocation
++ final long time = System.nanoTime();
++ this.chunkLoadTicketLimiter.reset(time);
++ this.chunkGenerateTicketLimiter.reset(time);
++ this.chunkSendLimiter.reset(time);
++
++ // now we can update
++ this.update();
++ }
++
++ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
++ return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ));
++ }
++
++ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
++ final BelowZeroRetrogen belowZeroRetrogen;
++ // see PortalForcer#findPortalAround
++ return chunkAccess != null && (
++ chunkAccess.getPersistedStatus() == ChunkStatus.FULL ||
++ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))
++ );
++ }
++
++ void update() {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
++ if (this.removed) {
++ throw new IllegalStateException("Updating removed player chunk loader");
++ }
++ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
++ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
++
++ final int tickViewDistance = getTickDistance(
++ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
++ playerDistances.loadViewDistance, worldDistances.loadViewDistance
++ );
++ // load view cannot be less-than tick view + 1
++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
++ // send view cannot be greater-than load view
++ final int clientViewDistance = getClientViewDistance(this.player);
++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
++
++ final ChunkPos playerPos = this.player.chunkPosition();
++ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
++ final int currentChunkX = playerPos.x;
++ final int currentChunkZ = playerPos.z;
++
++ final int prevChunkX = this.lastChunkX;
++ final int prevChunkZ = this.lastChunkZ;
++
++ if (
++ // has view distance stayed the same?
++ sendViewDistance == this.lastSendDistance
++ && loadViewDistance == this.lastLoadDistance
++ && tickViewDistance == this.lastTickDistance
++
++ // has our chunk stayed the same?
++ && prevChunkX == currentChunkX
++ && prevChunkZ == currentChunkZ
++
++ // can we still generate chunks?
++ && this.canGenerateChunks == canGenerateChunks
++ ) {
++ // nothing we care about changed, so we're not re-calculating
++ return;
++ }
++
++ // update distance maps
++ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1);
++ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
++ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
++ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) {
++ throw new IllegalStateException();
++ }
++
++ // update VDs for client
++ // this should be after the distance map updates, as they will send unload packets
++ if (this.lastSentChunkRadius != sendViewDistance) {
++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
++ }
++ if (this.lastSentSimulationDistance != tickViewDistance) {
++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
++ }
++
++ this.sendQueue.clear();
++ this.tickingQueue.clear();
++ this.generatingQueue.clear();
++ this.genQueue.clear();
++ this.loadingQueue.clear();
++ this.loadQueue.clear();
++
++ this.lastChunkX = currentChunkX;
++ this.lastChunkZ = currentChunkZ;
++ this.lastSendDistance = sendViewDistance;
++ this.lastLoadDistance = loadViewDistance;
++ this.lastTickDistance = tickViewDistance;
++ this.canGenerateChunks = canGenerateChunks;
++
++ // +1 since we need to load chunks +1 around the load view distance...
++ final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1);
++ // the iteration order is by increasing manhattan distance - so, we do NOT need to
++ // sort anything in the queue!
++ for (final long deltaChunk : toIterate) {
++ final int dx = CoordinateUtils.getChunkX(deltaChunk);
++ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
++ final int chunkX = dx + currentChunkX;
++ final int chunkZ = dz + currentChunkZ;
++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
++
++ // since chunk sending is not by radius alone, we need an extra check here to account for
++ // everything <= sendDistance
++ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
++ // the dist <= view check
++ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1))
++ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
++ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
++
++ if (!sendChunk && sentChunk) {
++ // have sent the chunk, but don't want it anymore
++ // unload it now
++ this.sendUnloadChunkRaw(chunkX, chunkZ);
++ }
++
++ final byte stage = this.chunkTicketStage.get(chunk);
++ switch (stage) {
++ case CHUNK_TICKET_STAGE_NONE: {
++ // we want the chunk to be at least loaded
++ this.loadQueue.enqueue(chunk);
++ break;
++ }
++ case CHUNK_TICKET_STAGE_LOADING: {
++ this.loadingQueue.enqueue(chunk);
++ break;
++ }
++ case CHUNK_TICKET_STAGE_LOADED: {
++ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
++ this.genQueue.enqueue(chunk);
++ }
++ break;
++ }
++ case CHUNK_TICKET_STAGE_GENERATING: {
++ this.generatingQueue.enqueue(chunk);
++ break;
++ }
++ case CHUNK_TICKET_STAGE_GENERATED: {
++ if (sendChunk && !sentChunk) {
++ this.sendQueue.enqueue(chunk);
++ }
++ if (squareDistance <= tickViewDistance) {
++ this.tickingQueue.enqueue(chunk);
++ }
++ break;
++ }
++ case CHUNK_TICKET_STAGE_TICK: {
++ if (sendChunk && !sentChunk) {
++ this.sendQueue.enqueue(chunk);
++ }
++ break;
++ }
++ default: {
++ throw new IllegalStateException("Unknown stage: " + stage);
++ }
++ }
++ }
++
++ // update the chunk center
++ // this must be done last so that the client does not ignore any of our unload chunk packets above
++ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
++ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
++ }
++
++ this.flushDelayedTicketOps();
++ }
++
++ void remove() {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
++ if (this.removed) {
++ throw new IllegalStateException("Removing removed player chunk loader");
++ }
++ this.removed = true;
++ // sends the chunk unload packets
++ this.broadcastMap.remove();
++ // cleans up loading/generating tickets
++ this.loadTicketCleanup.remove();
++ // cleans up ticking tickets
++ this.tickMap.remove();
++
++ // purge queues
++ this.sendQueue.clear();
++ this.tickingQueue.clear();
++ this.generatingQueue.clear();
++ this.genQueue.clear();
++ this.loadingQueue.clear();
++ this.loadQueue.clear();
++
++ // flush ticket changes
++ this.flushDelayedTicketOps();
++
++ // now all tickets should be removed, which is all of our external state
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bc07e710a5854fd526e3bb56d1565602ec728ce1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+@@ -0,0 +1,140 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.queue;
++
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import com.google.gson.JsonArray;
++import com.google.gson.JsonElement;
++import com.google.gson.JsonObject;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++import java.util.ArrayList;
++import java.util.Iterator;
++import java.util.List;
++import java.util.concurrent.atomic.AtomicLong;
++
++public final class ChunkUnloadQueue {
++
++ public final int coordinateShift;
++ private final AtomicLong orderGenerator = new AtomicLong();
++ private final ConcurrentLong2ReferenceChainedHashTable<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>();
++
++ /*
++ * Note: write operations do not occur in parallel for any given section.
++ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly
++ */
++
++ public ChunkUnloadQueue(final int coordinateShift) {
++ this.coordinateShift = coordinateShift;
+ }
+
-+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
++ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
++
++ public List<SectionToUnload> retrieveForAllRegions() {
++ final List<SectionToUnload> ret = new ArrayList<>();
++
++ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next();
++ final long key = entry.getKey();
++ final UnloadSection section = entry.getValue();
++ final int sectionX = CoordinateUtils.getChunkX(key);
++ final int sectionZ = CoordinateUtils.getChunkZ(key);
++
++ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
+ }
-+ this.convertToFullTask.setPriority(priority);
++
++ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
++ return Long.compare(s1.order, s2.order);
++ });
++
++ return ret;
+ }
+
-+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
++ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
++ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ));
++ }
++
++ public UnloadSection removeSection(final int sectionX, final int sectionZ) {
++ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ));
++ }
++
++ // write operation
++ public boolean addChunk(final int chunkX, final int chunkZ) {
++ // write operations do not occur in parallel for a given section
++ final int shift = this.coordinateShift;
++ final int sectionX = chunkX >> shift;
++ final int sectionZ = chunkZ >> shift;
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ UnloadSection section = this.unloadSections.get(chunkKey);
++ if (section == null) {
++ section = new UnloadSection(this.orderGenerator.getAndIncrement());
++ this.unloadSections.put(chunkKey, section);
++ }
++
++ return section.chunks.add(chunkKey);
++ }
++
++ // write operation
++ public boolean removeChunk(final int chunkX, final int chunkZ) {
++ // write operations do not occur in parallel for a given section
++ final int shift = this.coordinateShift;
++ final int sectionX = chunkX >> shift;
++ final int sectionZ = chunkZ >> shift;
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ final UnloadSection section = this.unloadSections.get(chunkKey);
++
++ if (section == null) {
++ return false;
++ }
++
++ if (!section.chunks.remove(chunkKey)) {
++ return false;
++ }
++
++ if (section.chunks.isEmpty()) {
++ this.unloadSections.remove(chunkKey);
++ }
++
++ return true;
++ }
++
++ public JsonElement toDebugJson() {
++ final JsonArray ret = new JsonArray();
++
++ for (final SectionToUnload section : this.retrieveForAllRegions()) {
++ final JsonObject sectionJson = new JsonObject();
++ ret.add(sectionJson);
++
++ sectionJson.addProperty("sectionX", section.sectionX());
++ sectionJson.addProperty("sectionZ", section.sectionX());
++ sectionJson.addProperty("order", section.order());
++
++ final JsonArray coordinates = new JsonArray();
++ sectionJson.add("coordinates", coordinates);
++
++ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ());
++ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final JsonObject coordinateJson = new JsonObject();
++ coordinates.add(coordinateJson);
++
++ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate)));
++ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate)));
++ }
++ }
++
++ return ret;
++ }
++
++ public static final class UnloadSection {
++
++ public final long order;
++ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet();
++
++ public UnloadSection(final long order) {
++ this.order = order;
+ }
-+ this.convertToFullTask.raisePriority(priority);
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e251bc4c4
+index 0000000000000000000000000000000000000000..a7e7569b9d4160e7d92422ca5c1cce7f46b78f2e
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
-@@ -0,0 +1,1500 @@
-+package io.papermc.paper.chunk.system.scheduling;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+@@ -0,0 +1,1430 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
++
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
-+import com.google.common.collect.ImmutableList;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket;
++import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader;
-+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
-+import io.papermc.paper.chunk.system.poi.PoiChunk;
-+import io.papermc.paper.threadedregions.TickRegions;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import io.papermc.paper.world.ChunkEntitySlices;
+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ByteMap;
-+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2IntMap;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
@@ -5489,8 +8665,6 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
+import net.minecraft.nbt.CompoundTag;
-+import io.papermc.paper.chunk.system.ChunkSystem;
-+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
@@ -5500,79 +8674,43 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+import net.minecraft.util.SortedArraySet;
+import net.minecraft.util.Unit;
+import net.minecraft.world.level.ChunkPos;
-+import org.bukkit.plugin.Plugin;
+import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
-+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
-+import java.util.concurrent.ConcurrentHashMap;
++import java.util.Objects;
++import java.util.PrimitiveIterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Predicate;
+
+public final class ChunkHolderManager {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class);
+
-+ public static final int FULL_LOADED_TICKET_LEVEL = 33;
-+ public static final int BLOCK_TICKING_TICKET_LEVEL = 32;
-+ public static final int ENTITY_TICKING_TICKET_LEVEL = 31;
++ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
++ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
++ public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL;
+ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive
+
++ public static final TicketType<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
++
+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
+ public final ReentrantAreaLock ticketLockArea;
+
-+ private final ConcurrentHashMap<RegionFileIOThread.ChunkCoordinate, SortedArraySet<Ticket<?>>> tickets = new java.util.concurrent.ConcurrentHashMap<>();
-+ private final ConcurrentHashMap<RegionFileIOThread.ChunkCoordinate, Long2IntOpenHashMap> sectionToChunkToExpireCount = new java.util.concurrent.ConcurrentHashMap<>();
-+ final ChunkQueue unloadQueue;
-+
-+ public boolean processTicketUpdates(final int posX, final int posZ) {
-+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
-+ final int ticketMask = (1 << ticketShift) - 1;
-+ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
-+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
-+ final boolean ret;
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
-+ ((posX >> ticketShift) - 1) << ticketShift,
-+ ((posZ >> ticketShift) - 1) << ticketShift,
-+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
-+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
-+ );
-+ try {
-+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ this.addChangedStatuses(changedFullStatus);
-+
-+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
-+ scheduledTasks.get(i).schedule();
-+ }
-+
-+ return ret;
-+ }
++ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>();
++ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>();
++ final ChunkUnloadQueue unloadQueue;
+
-+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks,
-+ final List<NewChunkHolder> changedFullStatus) {
-+ return this.ticketLevelPropagator.performUpdate(
-+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
-+ );
-+ }
-+
-+ private final SWMRLong2ObjectHashTable<NewChunkHolder> chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f);
-+ // what a disaster of a name
-+ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick
-+ private final Long2ObjectOpenHashMap<Long2IntOpenHashMap> removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>();
++ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
+ private final ServerLevel world;
+ private final ChunkTaskScheduler taskScheduler;
+ private long currentTick;
@@ -5603,27 +8741,56 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ this.world = world;
+ this.taskScheduler = taskScheduler;
+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift());
-+ this.unloadQueue = new ChunkQueue(world.getRegionChunkShift());
++ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
+ }
+
-+ private final AtomicLong statusUpgradeId = new AtomicLong();
++ public boolean processTicketUpdates(final int posX, final int posZ) {
++ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
++ final int ticketMask = (1 << ticketShift) - 1;
++ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
++ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
++ final boolean ret;
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
++ ((posX >> ticketShift) - 1) << ticketShift,
++ ((posZ >> ticketShift) - 1) << ticketShift,
++ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
++ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
++ );
++ try {
++ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
+
-+ long getNextStatusUpgradeId() {
-+ return this.statusUpgradeId.incrementAndGet();
++ this.addChangedStatuses(changedFullStatus);
++
++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
++ scheduledTasks.get(i).schedule();
++ }
++
++ return ret;
++ }
++
++ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks,
++ final List<NewChunkHolder> changedFullStatus) {
++ return this.ticketLevelPropagator.performUpdate(
++ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
++ );
+ }
+
+ public List<ChunkHolder> getOldChunkHolders() {
-+ final List<NewChunkHolder> holders = this.getChunkHolders();
-+ final List<ChunkHolder> ret = new ArrayList<>(holders.size());
-+ for (final NewChunkHolder holder : holders) {
-+ ret.add(holder.vanillaChunkHolder);
++ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
++ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
++ ret.add(iterator.next().vanillaChunkHolder);
+ }
+ return ret;
+ }
+
+ public List<NewChunkHolder> getChunkHolders() {
-+ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size());
-+ this.chunkHolders.forEachValue(ret::add);
++ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
++ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
++ ret.add(iterator.next());
++ }
+ return ret;
+ }
+
@@ -5631,14 +8798,35 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return this.chunkHolders.size();
+ }
+
++ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks
++ public Iterable<ChunkHolder> getOldChunkHoldersIterable() {
++ return new Iterable<ChunkHolder>() {
++ @Override
++ public Iterator<ChunkHolder> iterator() {
++ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator();
++ return new Iterator<ChunkHolder>() {
++ @Override
++ public boolean hasNext() {
++ return iterator.hasNext();
++ }
++
++ @Override
++ public ChunkHolder next() {
++ return iterator.next().vanillaChunkHolder;
++ }
++ };
++ }
++ };
++ }
++
+ public void close(final boolean save, final boolean halt) {
-+ TickThread.ensureTickThread("Closing world off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread("Closing world off-main");
+ if (halt) {
-+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'");
++ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
-+ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'");
++ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
+ } else {
-+ LOGGER.info("Halted chunk system for world '" + this.world.getWorld().getName() + "'");
++ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+ }
+
@@ -5646,40 +8834,40 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ this.saveAllChunks(true, true, true);
+ }
+
-+ if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) {
++ boolean hasTasks = false;
++ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
++ if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) {
++ hasTasks = true;
++ break;
++ }
++ }
++ if (hasTasks) {
+ RegionFileIOThread.flush();
+ }
+
+ // kill regionfile cache
-+ try {
-+ this.world.chunkDataControllerNew.getCache().close();
-+ } catch (final IOException ex) {
-+ LOGGER.error("Failed to close chunk regionfile cache for world '" + this.world.getWorld().getName() + "'", ex);
-+ }
-+ try {
-+ this.world.entityDataControllerNew.getCache().close();
-+ } catch (final IOException ex) {
-+ LOGGER.error("Failed to close entity regionfile cache for world '" + this.world.getWorld().getName() + "'", ex);
-+ }
-+ try {
-+ this.world.poiDataControllerNew.getCache().close();
-+ } catch (final IOException ex) {
-+ LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex);
++ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
++ try {
++ RegionFileIOThread.getControllerFor(this.world, type).getCache().close();
++ } catch (final IOException ex) {
++ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex);
++ }
+ }
+ }
+
+ void ensureInAutosave(final NewChunkHolder holder) {
+ if (!this.autoSaveQueue.contains(holder)) {
-+ holder.lastAutoSave = MinecraftServer.currentTick;
++ holder.lastAutoSave = this.currentTick;
+ this.autoSaveQueue.add(holder);
+ }
+ }
+
+ public void autoSave() {
+ final List<NewChunkHolder> reschedule = new ArrayList<>();
-+ final long currentTick = MinecraftServer.currentTickLong;
-+ final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value();
-+ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) {
++ final long currentTick = this.currentTick;
++ final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value());
++ final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick;
++ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
+ final NewChunkHolder holder = this.autoSaveQueue.first();
+
+ if (holder.lastAutoSave > maxSaveTime) {
@@ -5689,7 +8877,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ this.autoSaveQueue.remove(holder);
+
+ holder.lastAutoSave = currentTick;
-+ if (holder.save(false, false) != null) {
++ if (holder.save(false) != null) {
+ ++autoSaved;
+ }
+
@@ -5709,7 +8897,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final List<NewChunkHolder> holders = this.getChunkHolders();
+
+ if (logProgress) {
-+ LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'");
++ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ final DecimalFormat format = new DecimalFormat("#0.00");
@@ -5728,7 +8916,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ for (int i = 0, len = holders.size(); i < len; ++i) {
+ final NewChunkHolder holder = holders.get(i);
+ try {
-+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false);
++ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
+ if (saveStat != null) {
+ ++saved;
+ needsFlush = flush;
@@ -5742,10 +8930,8 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ ++savedPoi;
+ }
+ }
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr);
++ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
+ }
+ if (needsFlush && (saved % flushInterval) == 0) {
+ needsFlush = false;
@@ -5755,26 +8941,24 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final long currTime = System.nanoTime();
+ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) {
+ lastLog = currTime;
-+ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + this.world.getWorld().getName() + "'");
++ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+ }
+ }
+ if (flush) {
+ RegionFileIOThread.flush();
-+ if (this.world.paperConfig().chunks.flushRegionsOnSave) {
-+ try {
-+ this.world.chunkSource.chunkMap.regionFileCache.flush();
-+ } catch (IOException ex) {
-+ LOGGER.error("Exception when flushing regions in world {}", this.world.getWorld().getName(), ex);
-+ }
++ try {
++ RegionFileIOThread.flushRegionStorages(this.world);
++ } catch (final IOException ex) {
++ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex);
+ }
+ }
+ if (logProgress) {
-+ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + this.world.getWorld().getName() + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s");
++ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s");
+ }
+ }
+
-+ protected final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() {
++ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() {
+ @Override
+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) {
+ // first the necessary chunkholders must be created, so just update the ticket levels
@@ -5800,9 +8984,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ if (current == null) {
+ // must create
+ current = ChunkHolderManager.this.createChunkHolder(key);
-+ synchronized (ChunkHolderManager.this.chunkHolders) {
-+ ChunkHolderManager.this.chunkHolders.put(key, current);
-+ }
++ ChunkHolderManager.this.chunkHolders.put(key, current);
+ current.updateTicketLevel(newLevel);
+ } else {
+ current.updateTicketLevel(newLevel);
@@ -5843,7 +9025,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ public String getTicketDebugString(final long coordinate) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
+ try {
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate));
++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ return tickets != null ? tickets.first().toString() : "no_ticket";
+ } finally {
@@ -5855,38 +9037,40 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() {
+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap<List<RegionFileIOThread.ChunkCoordinate>> sections = new Long2ObjectOpenHashMap();
++ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
-+ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) {
++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
++ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
+ CoordinateUtils.getChunkKey(
-+ CoordinateUtils.getChunkX(coord.key) >> sectionShift,
-+ CoordinateUtils.getChunkZ(coord.key) >> sectionShift
++ CoordinateUtils.getChunkX(coord) >> sectionShift,
++ CoordinateUtils.getChunkZ(coord) >> sectionShift
+ ),
+ (final long keyInMap) -> {
-+ return new ArrayList<>();
++ return new LongArrayList();
+ }
+ ).add(coord);
+ }
+
-+ for (final Iterator<Long2ObjectMap.Entry<List<RegionFileIOThread.ChunkCoordinate>>> iterator = sections.long2ObjectEntrySet().fastIterator();
++ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
-+ final Long2ObjectMap.Entry<List<RegionFileIOThread.ChunkCoordinate>> entry = iterator.next();
++ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
-+ final List<RegionFileIOThread.ChunkCoordinate> coordinates = entry.getValue();
++ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
-+ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) {
++ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
++ final long coord = iterator2.nextLong();
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord);
+ if (tickets == null) {
+ // removed before we acquired lock
+ continue;
+ }
-+ ret.put(coord.key, new SortedArraySet<>(tickets));
++ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy());
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
@@ -5896,21 +9080,22 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return ret;
+ }
+
-+ public Collection<Plugin> getPluginChunkTickets(int x, int z) {
-+ ImmutableList.Builder<Plugin> ret;
++ // Paper start
++ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) {
++ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z);
+ try {
+ final long coordinate = CoordinateUtils.getChunkKey(x, z);
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate));
++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ if (tickets == null) {
-+ return Collections.emptyList();
++ return java.util.Collections.emptyList();
+ }
+
-+ ret = ImmutableList.builder();
++ ret = com.google.common.collect.ImmutableList.builder();
+ for (Ticket<?> ticket : tickets) {
+ if (ticket.getType() == TicketType.PLUGIN_TICKET) {
-+ ret.add((Plugin)ticket.key);
++ ret.add((org.bukkit.plugin.Plugin)ticket.key);
+ }
+ }
+ } finally {
@@ -5919,6 +9104,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ return ret.build();
+ }
++ // Paper end
+
+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
+ if (ticketLevel > ChunkLevel.MAX_LEVEL) {
@@ -5945,13 +9131,13 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ private void addExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
-+ final int sectionShift = this.world.getRegionChunkShift();
-+ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey(
++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
++ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
-+ ));
++ );
+
-+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> {
++ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> {
+ return new Long2IntOpenHashMap();
+ }).addTo(chunkKey, 1);
+ }
@@ -5959,11 +9145,11 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ private void removeExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
-+ final int sectionShift = this.world.getRegionChunkShift();
-+ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey(
++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
++ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
-+ ));
++ );
+
+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey);
+ final int prevCount = removeCounts.addTo(chunkKey, -1);
@@ -5990,21 +9176,21 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
-+ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk);
-+ final Ticket<T> ticket = new Ticket<>(type, level, identifier, removeDelay);
++ final Ticket<T> ticket = new Ticket<>(type, level, identifier);
++ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
-+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunkCoord, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> {
++ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
+ return SortedArraySet.create(4);
+ });
+
+ final int levelBefore = getTicketLevelAt(ticketsAtChunk);
-+ final Ticket<T> current = (Ticket<T>)ticketsAtChunk.replace(ticket);
++ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket);
+ final int levelAfter = getTicketLevelAt(ticketsAtChunk);
+
+ if (current != ticket) {
-+ final long oldRemoveDelay = current.removeDelay;
++ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay();
+ if (removeDelay != oldRemoveDelay) {
+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
@@ -6050,18 +9236,17 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
-+ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk);
-+ final Ticket<T> probe = new Ticket<>(type, level, identifier, PROBE_MARKER);
++ final Ticket<T> probe = new Ticket<>(type, level, identifier);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
-+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunkCoord);
++ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk);
+ if (ticketsAtChunk == null) {
+ return false;
+ }
+
+ final int oldLevel = getTicketLevelAt(ticketsAtChunk);
-+ final Ticket<T> ticket = (Ticket<T>)ticketsAtChunk.removeAndGet(probe);
++ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe);
+
+ if (ticket == null) {
+ return false;
@@ -6070,21 +9255,8 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final int newLevel = getTicketLevelAt(ticketsAtChunk);
+ // we should not change the ticket levels while the target region may be ticking
+ if (oldLevel != newLevel) {
-+ // Delay unload chunk patch originally by Aikar, updated to 1.20 by jpenilla
-+ // these days, the patch is mostly useful to keep chunks ticking when players teleport
-+ // so that their pets can teleport with them as well.
-+ final long delayTimeout = this.world.paperConfig().chunks.delayChunkUnloadsBy.ticks();
-+ final TicketType<ChunkPos> toAdd;
-+ final long timeout;
-+ if (type == RegionizedPlayerChunkLoader.REGION_PLAYER_TICKET && delayTimeout > 0) {
-+ toAdd = TicketType.DELAY_UNLOAD;
-+ timeout = delayTimeout;
-+ } else {
-+ toAdd = TicketType.UNKNOWN;
-+ // always expect UNKNOWN to be > 1, but just in case
-+ timeout = Math.max(1, toAdd.timeout);
-+ }
-+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(toAdd, level, new ChunkPos(chunk), timeout);
++ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk));
++ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout));
+ if (ticketsAtChunk.add(unknownTicket)) {
+ this.addExpireCount(chunkX, chunkZ);
+ } else {
@@ -6092,7 +9264,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+ }
+
-+ final long removeDelay = ticket.removeDelay;
++ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay();
+ if (removeDelay != NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
+ }
@@ -6137,33 +9309,35 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return;
+ }
+
-+ final Long2ObjectOpenHashMap<List<RegionFileIOThread.ChunkCoordinate>> sections = new Long2ObjectOpenHashMap();
++ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
-+ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) {
++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
++ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
-+ CoordinateUtils.getChunkKey(
-+ CoordinateUtils.getChunkX(coord.key) >> sectionShift,
-+ CoordinateUtils.getChunkZ(coord.key) >> sectionShift
-+ ),
-+ (final long keyInMap) -> {
-+ return new ArrayList<>();
-+ }
++ CoordinateUtils.getChunkKey(
++ CoordinateUtils.getChunkX(coord) >> sectionShift,
++ CoordinateUtils.getChunkZ(coord) >> sectionShift
++ ),
++ (final long keyInMap) -> {
++ return new LongArrayList();
++ }
+ ).add(coord);
+ }
+
-+ for (final Iterator<Long2ObjectMap.Entry<List<RegionFileIOThread.ChunkCoordinate>>> iterator = sections.long2ObjectEntrySet().fastIterator();
++ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
-+ final Long2ObjectMap.Entry<List<RegionFileIOThread.ChunkCoordinate>> entry = iterator.next();
++ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
-+ final List<RegionFileIOThread.ChunkCoordinate> coordinates = entry.getValue();
++ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
-+ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) {
-+ this.removeTicketAtLevel(ticketType, coord.key, ticketLevel, ticketIdentifier, false);
++ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
++ final long coord = iterator2.nextLong();
++ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
@@ -6172,20 +9346,22 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ public void tick() {
-+ final int sectionShift = this.world.getRegionChunkShift();
++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+
+ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> {
-+ if (ticket.removeDelay == NO_TIMEOUT_MARKER) {
++ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay();
++ if (removeDelay == NO_TIMEOUT_MARKER) {
+ return false;
+ }
-+ return --ticket.removeDelay <= 0L;
++ --removeDelay;
++ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
++ return removeDelay <= 0L;
+ };
+
-+ for (final Iterator<RegionFileIOThread.ChunkCoordinate> iterator = this.sectionToChunkToExpireCount.keySet().iterator(); iterator.hasNext();) {
-+ final RegionFileIOThread.ChunkCoordinate section = iterator.next();
-+ final long sectionKey = section.key;
++ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
++ final long sectionKey = iterator.nextLong();
+
-+ if (!this.sectionToChunkToExpireCount.containsKey(section)) {
++ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
+ // removed concurrently
+ continue;
+ }
@@ -6196,7 +9372,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ );
+
+ try {
-+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(section);
++ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey);
+ if (chunkToExpireCount == null) {
+ // lost to some race
+ continue;
@@ -6208,9 +9384,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final long chunkKey = entry.getLongKey();
+ final int expireCount = entry.getIntValue();
+
-+ final RegionFileIOThread.ChunkCoordinate chunk = new RegionFileIOThread.ChunkCoordinate(chunkKey);
-+
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunk);
++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey);
+ final int levelBefore = getTicketLevelAt(tickets);
+
+ final int sizeBefore = tickets.size();
@@ -6219,7 +9393,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final int levelAfter = getTicketLevelAt(tickets);
+
+ if (tickets.isEmpty()) {
-+ this.tickets.remove(chunk);
++ this.tickets.remove(chunkKey);
+ }
+ if (levelBefore != levelAfter) {
+ this.updateTicketLevel(chunkKey, levelAfter);
@@ -6239,7 +9413,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ if (chunkToExpireCount.isEmpty()) {
-+ this.sectionToChunkToExpireCount.remove(section);
++ this.sectionToChunkToExpireCount.remove(sectionKey);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
@@ -6282,7 +9456,6 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
+
+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
-+ ret.vanillaChunkHolder.onChunkAdd();
+
+ return ret;
+ }
@@ -6314,17 +9487,14 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ current = this.createChunkHolder(position);
-+ synchronized (this.chunkHolders) {
-+ this.chunkHolders.put(position, current);
-+ }
++ this.chunkHolders.put(position, current);
++
+
+ return current;
+ }
+
-+ private final AtomicLong entityLoadCounter = new AtomicLong();
-+
+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
+ ChunkEntitySlices ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
@@ -6334,32 +9504,32 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
-+ final Long entityLoadId = Long.valueOf(this.entityLoadCounter.getAndIncrement());
++ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
-+ this.addTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
++ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
-+ this.removeTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
++ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ return ret;
+ }
+
-+ if (current.isEntityChunkNBTLoaded()) {
-+ isCompleted.setPlain(true);
-+ } else {
-+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> {
-+ if (!transientChunk) {
++ if (!transientChunk) {
++ if (current.isEntityChunkNBTLoaded()) {
++ isCompleted.setPlain(true);
++ } else {
++ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> {
+ isCompleted.set(true);
+ LockSupport.unpark(waiter);
-+ }
-+ });
-+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
++ });
++ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
+
-+ if (entityLoad != null && !transientChunk) {
-+ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ if (entityLoad != null) {
++ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ }
+ }
+ }
+ } finally {
@@ -6390,11 +9560,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+
+ ret = current.loadInEntityChunk(transientChunk);
+
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ this.addAndRemoveTickets(chunkKey,
-+ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ),
-+ TicketType.ENTITY_LOAD, MAX_TICKET_LEVEL, entityLoadId
-+ );
++ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+
+ return ret;
+ }
@@ -6408,79 +9574,67 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return null;
+ }
+
-+ private final AtomicLong poiLoadCounter = new AtomicLong();
-+
+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
+ PoiChunk ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
+ if (current != null && (ret = current.getPoiChunk()) != null) {
-+ if (!ret.isLoaded()) {
-+ ret.load();
-+ }
++ ret.load();
+ return ret;
+ }
+
+ final AtomicReference<PoiChunk> completed = new AtomicReference<>();
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
-+ final Long poiLoadId = Long.valueOf(this.poiLoadCounter.getAndIncrement());
++ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
-+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
-+ // Folia - use area based lock to reduce contention
-+ this.addTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
-+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention
++ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
++ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
-+ if (current.isPoiChunkLoaded()) {
-+ this.removeTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
-+ return current.getPoiChunk();
-+ }
-+
-+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> {
-+ completed.setPlain(result.left());
-+ isCompleted.set(true);
-+ LockSupport.unpark(waiter);
-+ });
-+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
++ if (null == (ret = current.getPoiChunk())) {
++ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> {
++ completed.setPlain(result.left());
++ isCompleted.set(true);
++ LockSupport.unpark(waiter);
++ });
++ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
+
-+ if (poiLoad != null) {
-+ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ if (poiLoad != null) {
++ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ }
+ }
+ } finally {
-+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); // Folia - use area based lock to reduce contention
++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
-+ this.ticketLockArea.unlock(ticketLock); // Folia - use area based lock to reduce contention
++ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadTask != null) {
+ loadTask.schedule();
-+ }
+
-+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main
++ // Note: no need to busy wait on the chunk queue, poi load will complete off-main
+
-+ boolean interrupted = false;
-+ while (!isCompleted.get()) {
-+ interrupted |= Thread.interrupted();
-+ LockSupport.park();
-+ }
++ boolean interrupted = false;
++ while (!isCompleted.get()) {
++ interrupted |= Thread.interrupted();
++ LockSupport.park();
++ }
+
-+ if (interrupted) {
-+ Thread.currentThread().interrupt();
-+ }
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
+
-+ ret = completed.getPlain();
++ ret = completed.getPlain();
++ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked
+
+ ret.load();
+
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ this.addAndRemoveTickets(chunkKey,
-+ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ),
-+ TicketType.POI_LOAD, MAX_TICKET_LEVEL, poiLoadId
-+ );
++ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
+
+ return ret;
+ }
@@ -6489,7 +9643,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ if (changedFullStatus.isEmpty()) {
+ return;
+ }
-+ if (!TickThread.isTickThread()) {
++ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.taskScheduler.scheduleChunkTask(() -> {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -6507,27 +9661,25 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ private void removeChunkHolder(final NewChunkHolder holder) {
-+ holder.killed = true;
-+ holder.vanillaChunkHolder.onChunkRemove();
++ holder.markUnloaded();
+ this.autoSaveQueue.remove(holder);
+ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
-+ synchronized (this.chunkHolders) {
-+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
-+ }
++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
++
+ }
+
+ // note: never call while inside the chunk system, this will absolutely break everything
+ public void processUnloads() {
-+ TickThread.ensureTickThread("Cannot unload chunks off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot unload chunks off-main");
+
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot unload chunks recursively");
+ }
+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
-+ final List<ChunkQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
++ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
+ int unloadCountTentative = 0;
-+ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
-+ final ChunkQueue.UnloadSection section
++ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
++ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
@@ -6546,14 +9698,13 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return;
+ }
+
-+ // Note: The behaviour that we process ticket updates while holding the lock has been dropped here, as it is racey behavior.
-+ // But, we do need to process updates here so that any add ticket that is synchronised before this call does not go missed.
++ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed.
+ this.processTicketUpdates();
+
+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05));
+ int processedCount = 0;
+
-+ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
++ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final List<NewChunkHolder> stage1 = new ArrayList<>();
+ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>();
+
@@ -6565,7 +9716,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ try {
+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
-+ final ChunkQueue.UnloadSection section
++ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
@@ -6604,6 +9755,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ // run stage 1
+ for (int i = 0, len = stage1.size(); i < len; ++i) {
+ final NewChunkHolder chunkHolder = stage1.get(i);
++ chunkHolder.removeFromUnloadQueue();
+ if (chunkHolder.isSafeToUnload() != null) {
+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?");
+ continue;
@@ -6651,7 +9803,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ this.removeChunkHolder(holder);
+ } else {
+ // add cooldown so the next unload check is not immediately next tick
-+ this.addTicketAtLevel(TicketType.UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
++ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
+ }
+ }
+ } finally {
@@ -6727,7 +9879,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+ }
+
-+ private boolean processTicketOp(TicketOperation operation) {
++ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) {
+ boolean ret = false;
+ switch (operation.op) {
+ case ADD: {
@@ -6781,7 +9933,7 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ public boolean processTicketUpdates() {
-+ return this.processTicketUpdates(true, true, null);
++ return this.processTicketUpdates(true, null);
+ }
+
+ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
@@ -6790,15 +9942,15 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return CURRENT_TICKET_UPDATE_SCHEDULING.get();
+ }
+
-+ private boolean processTicketUpdates(final boolean checkLocks, final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
-+ TickThread.ensureTickThread("Cannot process ticket levels off-main");
++ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot process ticket levels off-main");
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
+ }
+
+ List<NewChunkHolder> changedFullStatus = null;
+
-+ final boolean isTickThread = TickThread.isTickThread();
++ final boolean isTickThread = io.papermc.paper.util.TickThread.isTickThread();
+
+ boolean ret = false;
+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
@@ -6834,12 +9986,12 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ }
+
+ // only call on tick thread
-+ protected final boolean processPendingFullUpdate() {
++ private boolean processPendingFullUpdate() {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+
+ boolean ret = false;
+
-+ List<NewChunkHolder> changedFullStatus = new ArrayList<>();
++ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
+
+ NewChunkHolder holder;
+ while ((holder = pendingFullLoadUpdate.poll()) != null) {
@@ -6856,40 +10008,14 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ return ret;
+ }
+
-+ public JsonObject getDebugJsonForWatchdog() {
-+ return this.getDebugJsonNoLock();
-+ }
-+
-+ private JsonObject getDebugJsonNoLock() {
++ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
-+ ret.addProperty("current_tick", Long.valueOf(this.currentTick));
+
-+ final JsonArray unloadQueue = new JsonArray();
-+ ret.add("unload_queue", unloadQueue);
+ ret.addProperty("lock_shift", Integer.valueOf(this.taskScheduler.getChunkSystemLockShift()));
+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT));
-+ ret.addProperty("region_shift", Integer.valueOf(this.world.getRegionChunkShift()));
-+ for (final ChunkQueue.SectionToUnload section : this.unloadQueue.retrieveForAllRegions()) {
-+ final JsonObject sectionJson = new JsonObject();
-+ unloadQueue.add(sectionJson);
-+ sectionJson.addProperty("sectionX", section.sectionX());
-+ sectionJson.addProperty("sectionZ", section.sectionX());
-+ sectionJson.addProperty("order", section.order());
-+
-+ final JsonArray coordinates = new JsonArray();
-+ sectionJson.add("coordinates", coordinates);
++ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift()));
+
-+ final ChunkQueue.UnloadSection actualSection = this.unloadQueue.getSectionUnsynchronized(section.sectionX(), section.sectionZ());
-+ for (final LongIterator iterator = actualSection.chunks.iterator(); iterator.hasNext();) {
-+ final long coordinate = iterator.nextLong();
-+
-+ final JsonObject coordinateJson = new JsonObject();
-+ coordinates.add(coordinateJson);
-+
-+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate)));
-+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate)));
-+ }
-+ }
++ ret.add("unload_queue", this.unloadQueue.toDebugJson());
+
+ final JsonArray holders = new JsonArray();
+ ret.add("chunkholders", holders);
@@ -6898,41 +10024,13 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ holders.add(holder.getDebugJson());
+ }
+
-+ // TODO
-+ /*
-+ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray();
-+ ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount);
-+
-+ for (final Long2ObjectMap.Entry<Long2IntOpenHashMap> tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) {
-+ final long tick = tickEntry.getLongKey();
-+ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue();
-+
-+ final JsonObject tickJson = new JsonObject();
-+ removeTickToChunkExpireTicketCount.add(tickJson);
-+
-+ tickJson.addProperty("tick", Long.valueOf(tick));
-+
-+ final JsonArray tickEntries = new JsonArray();
-+ tickJson.add("entries", tickEntries);
-+
-+ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) {
-+ final long coordinate = entry.getLongKey();
-+ final int count = entry.getIntValue();
-+
-+ final JsonObject entryJson = new JsonObject();
-+ tickEntries.add(entryJson);
-+
-+ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
-+ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
-+ entryJson.addProperty("count", Integer.valueOf(count));
-+ }
-+ }
-+
+ final JsonArray allTicketsJson = new JsonArray();
+ ret.add("tickets", allTicketsJson);
+
-+ for (final Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> coordinateTickets : this.tickets.long2ObjectEntrySet()) {
-+ final long coordinate = coordinateTickets.getLongKey();
++ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator();
++ iterator.hasNext();) {
++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next();
++ final long coordinate = coordinateTickets.getKey();
+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
+
+ final JsonObject coordinateJson = new JsonObject();
@@ -6944,1019 +10042,77 @@ index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e
+ final JsonArray ticketsSerialized = new JsonArray();
+ coordinateJson.add("tickets", ticketsSerialized);
+
-+ for (final Ticket<?> ticket : tickets) {
++ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating
++ // directly over the set using the iterator
++ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an
++ // entry OR iterate over an entry multiple times
++ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) {
++ if (ticketUncasted == null) {
++ continue;
++ }
++ final Ticket<?> ticket = (Ticket<?>)ticketUncasted;
+ final JsonObject ticketSerialized = new JsonObject();
+ ticketsSerialized.add(ticketSerialized);
+
+ ticketSerialized.addProperty("type", ticket.getType().toString());
+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
-+ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick));
++ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay()));
+ }
+ }
-+ */
+
+ return ret;
+ }
-+
-+ public JsonObject getDebugJson() {
-+ return this.getDebugJsonNoLock(); // Folia - use area based lock to reduce contention
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..86e618586d2ad9d843ad761b7336bb3073ed4c23
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java
-@@ -0,0 +1,181 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.starlight.common.light.StarLightEngine;
-+import ca.spottedleaf.starlight.common.light.StarLightInterface;
-+import io.papermc.paper.chunk.system.light.LightQueue;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import org.apache.logging.log4j.LogManager;
-+import org.apache.logging.log4j.Logger;
-+import java.util.function.BooleanSupplier;
-+
-+public final class ChunkLightTask extends ChunkProgressionTask {
-+
-+ private static final Logger LOGGER = LogManager.getLogger();
-+
-+ protected final ChunkAccess fromChunk;
-+
-+ private final LightTaskPriorityHolder priorityHolder;
-+
-+ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
-+ this.fromChunk = chunk;
-+ }
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.priorityHolder.isScheduled();
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.LIGHT;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ this.priorityHolder.schedule();
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ this.priorityHolder.cancel();
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
-+ return this.priorityHolder.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ this.priorityHolder.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ this.priorityHolder.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ this.priorityHolder.raisePriority(priority);
-+ }
-+
-+ private static final class LightTaskPriorityHolder extends PriorityHolder {
-+
-+ protected final ChunkLightTask task;
-+
-+ protected LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
-+ super(priority);
-+ this.task = task;
-+ }
-+
-+ @Override
-+ protected void cancelScheduled() {
-+ final ChunkLightTask task = this.task;
-+ task.complete(null, null);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.Priority getScheduledPriority() {
-+ final ChunkLightTask task = this.task;
-+ return task.world.getChunkSource().getLightEngine().theLightEngine.lightQueue.getPriority(task.chunkX, task.chunkZ);
-+ }
-+
-+ @Override
-+ protected void scheduleTask(final PrioritisedExecutor.Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine;
-+ final LightQueue lightQueue = starLightInterface.lightQueue;
-+ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority);
-+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine;
-+ final LightQueue lightQueue = starLightInterface.lightQueue;
-+ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine;
-+ final LightQueue lightQueue = starLightInterface.lightQueue;
-+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine;
-+ final LightQueue lightQueue = starLightInterface.lightQueue;
-+ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority);
-+ }
-+ }
-+
-+ private static final class LightTask implements BooleanSupplier {
-+
-+ protected final StarLightInterface lightEngine;
-+ protected final ChunkLightTask task;
-+
-+ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) {
-+ this.lightEngine = lightEngine;
-+ this.task = task;
-+ }
-+
-+ @Override
-+ public boolean getAsBoolean() {
-+ final ChunkLightTask task = this.task;
-+ // executed on light thread
-+ if (!task.priorityHolder.markExecuting()) {
-+ // cancelled
-+ return false;
-+ }
-+
-+ try {
-+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk);
-+
-+ if (task.fromChunk.isLightCorrect() && task.fromChunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections);
-+ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ);
-+ } else {
-+ task.fromChunk.setLightCorrect(false);
-+ this.lightEngine.lightChunk(task.fromChunk, emptySections);
-+ task.fromChunk.setLightCorrect(true);
-+ }
-+ // we need to advance status
-+ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getStatus() == ChunkStatus.LIGHT.getParent()) {
-+ chunk.setStatus(ChunkStatus.LIGHT);
-+ }
-+ } catch (final Throwable thr) {
-+ if (!(thr instanceof ThreadDeath)) {
-+ LOGGER.fatal("Failed to light chunk " + task.fromChunk.getPos().toString() + " in world '" + this.lightEngine.getWorld().getWorld().getName() + "'", thr);
-+ }
-+
-+ task.complete(null, thr);
-+
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+
-+ return true;
-+ }
-+
-+ task.complete(task.fromChunk, null);
-+ return true;
-+ }
-+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5ff994d5af24b0bdd7b3a16e245b2c4100bef3f0
+index 0000000000000000000000000000000000000000..f52e104b3e07825caf0d6d1bda2e45c8437d6e20
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java
-@@ -0,0 +1,484 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.dataconverter.minecraft.MCDataConverter;
-+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
-+import io.papermc.paper.chunk.system.poi.PoiChunk;
-+import net.minecraft.SharedConstants;
-+import net.minecraft.core.registries.Registries;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.UpgradeData;
-+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
-+import net.minecraft.world.level.levelgen.blending.BlendingData;
-+import org.slf4j.Logger;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.function.Consumer;
-+
-+public final class ChunkLoadTask extends ChunkProgressionTask {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ private final NewChunkHolder chunkHolder;
-+ private final ChunkDataLoadTask loadTask;
-+
-+ private volatile boolean cancelled;
-+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
-+ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
-+ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult;
-+ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data
-+
-+ protected ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ this.chunkHolder = chunkHolder;
-+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
-+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> {
-+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
-+ ChunkLoadTask.this.tryCompleteLoad();
-+ });
-+ }
-+
-+ private void tryCompleteLoad() {
-+ if (this.taskCountToComplete.decrementAndGet() == 0) {
-+ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement
-+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
-+ }
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.EMPTY;
-+ }
-+
-+ private boolean scheduled;
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
-+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
-+
-+ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> {
-+ ChunkLoadTask.this.tryCompleteLoad();
-+ };
-+
-+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because
-+ // they must schedule a task to off main or to on main to complete
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ if (this.scheduled) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+ this.scheduled = true;
-+ if (this.cancelled) {
-+ return;
-+ }
-+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) {
-+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask);
-+ } else {
-+ entityLoadTask = null;
-+ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled
-+ }
-+
-+ if (!this.chunkHolder.isPoiChunkLoaded()) {
-+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask);
-+ } else {
-+ poiLoadTask = null;
-+ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled
-+ }
-+
-+ this.entityLoadTask = entityLoadTask;
-+ this.poiLoadTask = poiLoadTask;
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ if (entityLoadTask != null) {
-+ entityLoadTask.schedule();
-+ }
-+
-+ if (poiLoadTask != null) {
-+ poiLoadTask.schedule();
-+ }
-+
-+ this.loadTask.schedule(false);
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ // must be before load task access, so we can synchronise with the writes to the fields
-+ final boolean scheduled;
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ // must read field here, as it may be written later conucrrently -
-+ // we need to know if we scheduled _before_ cancellation
-+ scheduled = this.scheduled;
-+ this.cancelled = true;
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ /*
-+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled,
-+ so we need to manually try to complete in those cases
-+ It is also important to note that we set the cancelled field first, just in case
-+ the chunk load task attempts to complete with a non-null value
-+ */
-+
-+ if (scheduled) {
-+ // since we scheduled, we need to cancel the tasks
-+ if (this.entityLoadTask != null) {
-+ if (this.entityLoadTask.cancel()) {
-+ this.tryCompleteLoad();
-+ }
-+ }
-+ if (this.poiLoadTask != null) {
-+ if (this.poiLoadTask.cancel()) {
-+ this.tryCompleteLoad();
-+ }
-+ }
-+ } else {
-+ // since nothing was scheduled, we need to decrement the task count here ourselves
-+
-+ // for entity load task
-+ this.tryCompleteLoad();
-+
-+ // for poi load task
-+ this.tryCompleteLoad();
-+ }
-+ this.loadTask.cancel();
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
-+ return this.loadTask.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.lowerPriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.lowerPriority(priority);
-+ }
-+
-+ this.loadTask.lowerPriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.setPriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.setPriority(priority);
-+ }
-+
-+ this.loadTask.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.raisePriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.raisePriority(priority);
-+ }
-+
-+ this.loadTask.raisePriority(priority);
-+ }
-+
-+ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> {
-+
-+ private TaskResult<FinalCompletion, Throwable> result;
-+ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>();
-+
-+ protected volatile boolean completed;
-+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class);
-+
-+ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, type, priority);
-+ }
-+
-+ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) {
-+ if (!this.waiters.add(consumer)) {
-+ try {
-+ consumer.accept(this.result);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right())
-+ ), throwable);
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) {
-+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) {
-+ throw new IllegalStateException("Already completed");
-+ }
-+ this.result = result;
-+ Consumer<TaskResult<FinalCompletion, Throwable>> consumer;
-+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
-+ try {
-+ consumer.accept(result);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right())
-+ ), throwable);
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ return;
-+ }
-+ }
-+ }
-+ }
-+
-+ public static final class ChunkDataLoadTask extends CallbackDataLoadTask<ChunkAccess, ChunkAccess> {
-+ protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return false;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ChunkAccess data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ private ProtoChunk getEmptyChunk() {
-+ return new ProtoChunk(
-+ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world,
-+ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null
-+ );
-+ }
-+
-+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable);
-+ return new TaskResult<>(this.getEmptyChunk(), null);
-+ }
-+
-+ if (data == null) {
-+ return new TaskResult<>(this.getEmptyChunk(), null);
-+ }
-+
-+ // need to convert data, and then deserialize it
-+
-+ try {
-+ final ChunkPos chunkPos = new ChunkPos(this.chunkX, this.chunkZ);
-+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap;
-+ // run converters
-+ // note: upgradeChunkTag copies the data already
-+ final CompoundTag converted = chunkMap.upgradeChunkTag(
-+ this.world.getTypeKey(), chunkMap.overworldDataStorage, data, chunkMap.generator.getTypeNameForDataFixer(),
-+ chunkPos, this.world
-+ );
-+ // deserialize
-+ final ChunkSerializer.InProgressChunkHolder chunkHolder = ChunkSerializer.readInProgressChunkHolder(
-+ this.world, chunkMap.getPoiManager(), chunkPos, converted
-+ );
-+
-+ return new TaskResult<>(chunkHolder.protoChunk, null);
-+ } catch (final ThreadDeath death) {
-+ throw death;
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
-+ return new TaskResult<>(this.getEmptyChunk(), null);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> runOnMain(final ChunkAccess data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+
-+ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> {
-+ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return false;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> runOffMain(CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable);
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+
-+ if (data == null || data.isEmpty()) {
-+ // nothing to do
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+
-+ try {
-+ data = data.copy(); // coming from the I/O thread, so we need to copy
-+ // run converters
-+ final int dataVersion = !data.contains(SharedConstants.DATA_VERSION_TAG, net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) ? 1945 : data.getInt(SharedConstants.DATA_VERSION_TAG);
-+ final CompoundTag converted = MCDataConverter.convertTag(
-+ MCTypeRegistry.POI_CHUNK, data, dataVersion, SharedConstants.getCurrentVersion().getDataVersion().getVersion()
-+ );
-+
-+ // now we need to parse it
-+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null);
-+ } catch (final ThreadDeath death) {
-+ throw death;
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2);
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+
-+ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
-+
-+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return false;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable);
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ if (data == null || data.isEmpty()) {
-+ // nothing to do
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ try {
-+ // note: data comes from the I/O thread, so we need to copy it
-+ return new TaskResult<>(EntityStorage.upgradeChunkTag(data.copy()), null);
-+ } catch (final ThreadDeath death) {
-+ throw death;
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2);
-+ return new TaskResult<>(null, thr2);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..b2341328bb22f08836ef18785dc27393a36ce8d6
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java
-@@ -0,0 +1,105 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.function.BiConsumer;
-+
-+public abstract class ChunkProgressionTask {
-+
-+ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>();
-+ private ChunkAccess completedChunk;
-+ private Throwable completedThrowable;
-+
-+ protected final ChunkTaskScheduler scheduler;
-+ protected final ServerLevel world;
-+ protected final int chunkX;
-+ protected final int chunkZ;
-+
-+ protected volatile boolean completed;
-+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class);
-+
-+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) {
-+ this.scheduler = scheduler;
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ }
-+
-+ // Used only for debug json
-+ public abstract boolean isScheduled();
-+
-+ // Note: It is the responsibility of the task to set the chunk's status once it has completed
-+ public abstract ChunkStatus getTargetStatus();
-+
-+ /* Only executed once */
-+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */
-+ public abstract void schedule();
-+
-+ /* May be called multiple times */
-+ public abstract void cancel();
-+
-+ public abstract PrioritisedExecutor.Priority getPriority();
-+
-+ /* Schedule lock is always held for the priority update calls */
-+
-+ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
-+
-+ public abstract void setPriority(final PrioritisedExecutor.Priority priority);
-+
-+ public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
-+
-+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
-+ if (!this.waiters.add(onComplete)) {
-+ try {
-+ onComplete.accept(this.completedChunk, this.completedThrowable);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable)
-+ ), throwable);
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) {
-+ try {
-+ this.complete0(chunk, throwable);
-+ } catch (final Throwable thr2) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable)
-+ ), thr2);
-+ if (thr2 instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr2;
-+ }
-+ }
-+ }
-+
-+ private void complete0(final ChunkAccess chunk, final Throwable throwable) {
-+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) {
-+ throw new IllegalStateException("Already completed");
-+ }
-+ this.completedChunk = chunk;
-+ this.completedThrowable = throwable;
-+
-+ BiConsumer<ChunkAccess, Throwable> consumer;
-+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
-+ consumer.accept(chunk, throwable);
-+ }
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + this.world.getWorld().getName() +
-+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
-+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}";
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4cc1b3ba6d093a9683dbd8b7fe76106ae391e019
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java
-@@ -0,0 +1,160 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import it.unimi.dsi.fastutil.HashCommon;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.concurrent.atomic.AtomicLong;
-+
-+public final class ChunkQueue {
-+
-+ public final int coordinateShift;
-+ private final AtomicLong orderGenerator = new AtomicLong();
-+ private final ConcurrentHashMap<Coordinate, UnloadSection> unloadSections = new ConcurrentHashMap<>();
-+
-+ /*
-+ * Note: write operations do not occur in parallel for any given section.
-+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly
-+ */
-+
-+ public ChunkQueue(final int coordinateShift) {
-+ this.coordinateShift = coordinateShift;
-+ }
-+
-+ public static record SectionToUnload(int sectionX, int sectionZ, Coordinate coord, long order, int count) {}
-+
-+ public List<SectionToUnload> retrieveForAllRegions() {
-+ final List<SectionToUnload> ret = new ArrayList<>();
-+
-+ for (final Map.Entry<Coordinate, UnloadSection> entry : this.unloadSections.entrySet()) {
-+ final Coordinate coord = entry.getKey();
-+ final long key = coord.key;
-+ final UnloadSection section = entry.getValue();
-+ final int sectionX = Coordinate.x(key);
-+ final int sectionZ = Coordinate.z(key);
-+
-+ ret.add(new SectionToUnload(sectionX, sectionZ, coord, section.order, section.chunks.size()));
-+ }
-+
-+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
-+ return Long.compare(s1.order, s2.order);
-+ });
-+
-+ return ret;
-+ }
-+
-+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
-+ return this.unloadSections.get(coordinate);
-+ }
-+
-+ public UnloadSection removeSection(final int sectionX, final int sectionZ) {
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
-+ return this.unloadSections.remove(coordinate);
-+ }
-+
-+ // write operation
-+ public boolean addChunk(final int chunkX, final int chunkZ) {
-+ final int shift = this.coordinateShift;
-+ final int sectionX = chunkX >> shift;
-+ final int sectionZ = chunkZ >> shift;
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
-+ final long chunkKey = Coordinate.key(chunkX, chunkZ);
-+
-+ UnloadSection section = this.unloadSections.get(coordinate);
-+ if (section == null) {
-+ section = new UnloadSection(this.orderGenerator.getAndIncrement());
-+ // write operations do not occur in parallel for a given section
-+ this.unloadSections.put(coordinate, section);
-+ }
-+
-+ return section.chunks.add(chunkKey);
-+ }
-+
-+ // write operation
-+ public boolean removeChunk(final int chunkX, final int chunkZ) {
-+ final int shift = this.coordinateShift;
-+ final int sectionX = chunkX >> shift;
-+ final int sectionZ = chunkZ >> shift;
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
-+ final long chunkKey = Coordinate.key(chunkX, chunkZ);
-+
-+ final UnloadSection section = this.unloadSections.get(coordinate);
-+
-+ if (section == null) {
-+ return false;
-+ }
-+
-+ if (!section.chunks.remove(chunkKey)) {
-+ return false;
-+ }
-+
-+ if (section.chunks.isEmpty()) {
-+ this.unloadSections.remove(coordinate);
-+ }
-+
-+ return true;
-+ }
-+
-+ public static final class UnloadSection {
-+
-+ public final long order;
-+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet();
-+
-+ public UnloadSection(final long order) {
-+ this.order = order;
-+ }
-+ }
-+
-+ private static final class Coordinate implements Comparable<Coordinate> {
-+
-+ public final long key;
-+
-+ public Coordinate(final long key) {
-+ this.key = key;
-+ }
-+
-+ public Coordinate(final int x, final int z) {
-+ this.key = key(x, z);
-+ }
-+
-+ public static long key(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
-+
-+ public static int x(final long key) {
-+ return (int)key;
-+ }
-+
-+ public static int z(final long key) {
-+ return (int)(key >>> 32);
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ return (int)HashCommon.mix(this.key);
-+ }
-+
-+ @Override
-+ public boolean equals(final Object obj) {
-+ if (this == obj) {
-+ return true;
-+ }
-+
-+ if (!(obj instanceof Coordinate other)) {
-+ return false;
-+ }
-+
-+ return this.key == other.key;
-+ }
-+
-+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain
-+ // is too large. So we should implement compareTo to help.
-+ @Override
-+ public int compareTo(final Coordinate other) {
-+ return Long.compare(this.key, other.key);
-+ }
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f4926d778
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
-@@ -0,0 +1,883 @@
-+package io.papermc.paper.chunk.system.scheduling;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+@@ -0,0 +1,923 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
-+import io.papermc.paper.chunk.system.scheduling.queue.RadiusAwarePrioritisedExecutor;
-+import io.papermc.paper.configuration.GlobalConfiguration;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import java.util.function.BooleanSupplier;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer;
++import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep;
++import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
++import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.LevelChunk;
-+import org.bukkit.Bukkit;
++import net.minecraft.world.level.chunk.status.ChunkPyramid;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.chunk.status.ChunkStep;
+import org.slf4j.Logger;
-+import java.io.File;
++import org.slf4j.LoggerFactory;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
-+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
@@ -7966,47 +10122,33 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+
+public final class ChunkTaskScheduler {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class);
+
+ static int newChunkSystemIOThreads;
-+ static int newChunkSystemWorkerThreads;
+ static int newChunkSystemGenParallelism;
++ static int newChunkSystemGenPopulationParallelism;
+ static int newChunkSystemLoadParallelism;
+
-+ public static ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool workerThreads;
-+
+ private static boolean initialised = false;
+
-+ public static void init(final GlobalConfiguration.ChunkSystem config) {
++ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
+ if (initialised) {
+ return;
+ }
+ initialised = true;
-+ newChunkSystemIOThreads = config.ioThreads;
-+ newChunkSystemWorkerThreads = config.workerThreads;
-+ if (newChunkSystemIOThreads < 0) {
++ MoonriseCommon.init(chunkSystem); // Paper
++ newChunkSystemIOThreads = chunkSystem.ioThreads;
++ if (newChunkSystemIOThreads <= 0) {
+ newChunkSystemIOThreads = 1;
+ } else {
+ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads);
+ }
-+ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
-+ if (defaultWorkerThreads <= 4) {
-+ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
-+ } else {
-+ defaultWorkerThreads = defaultWorkerThreads / 2;
-+ }
-+ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
-+
-+ if (newChunkSystemWorkerThreads < 0) {
-+ newChunkSystemWorkerThreads = defaultWorkerThreads;
-+ } else {
-+ newChunkSystemWorkerThreads = Math.max(1, newChunkSystemWorkerThreads);
-+ }
+
-+ String newChunkSystemGenParallelism = config.genParallelism;
++ String newChunkSystemGenParallelism = chunkSystem.genParallelism;
+ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) {
+ newChunkSystemGenParallelism = "true";
+ }
++
+ boolean useParallelGen;
+ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled")
+ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) {
@@ -8018,19 +10160,46 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]");
+ }
+
-+ ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? newChunkSystemWorkerThreads : 1;
-+ ChunkTaskScheduler.newChunkSystemLoadParallelism = newChunkSystemWorkerThreads;
++ ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS;
++ ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1;
++ ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS;
+
+ RegionFileIOThread.init(newChunkSystemIOThreads);
-+ workerThreads = new ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool(
-+ "Paper Chunk System Worker Pool", newChunkSystemWorkerThreads,
-+ (final Thread thread, final Integer id) -> {
-+ thread.setPriority(Thread.NORM_PRIORITY - 2);
-+ thread.setName("Tuinity Chunk System Worker #" + id.intValue());
-+ thread.setUncaughtExceptionHandler(io.papermc.paper.chunk.system.scheduling.NewChunkHolder.CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER);
-+ }, (long)(20.0e6)); // 20ms
+
-+ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + newChunkSystemWorkerThreads + " worker threads, and gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenParallelism + " threads");
++ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads");
++ }
++
++ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo);
++ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong();
++
++ public static Long getNextChunkLoadId() {
++ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement());
++ }
++
++ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo);
++ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong();
++
++ public static Long getNextNonFullLoadId() {
++ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement());
++ }
++
++ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo);
++ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong();
++
++ public static Long getNextEntityLoadId() {
++ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement());
++ }
++
++ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo);
++ private static final AtomicLong POI_LOAD_IDS = new AtomicLong();
++
++ public static Long getNextPoiLoadId() {
++ return Long.valueOf(POI_LOAD_IDS.getAndIncrement());
++ }
++
++
++ public static int getTicketLevel(final ChunkStatus status) {
++ return ChunkLevel.byStatus(status);
+ }
+
+ public final ServerLevel world;
@@ -8045,18 +10214,27 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ public final ChunkHolderManager chunkHolderManager;
+
+ static {
-+ ChunkStatus.EMPTY.writeRadius = 0;
-+ ChunkStatus.STRUCTURE_STARTS.writeRadius = 0;
-+ ChunkStatus.STRUCTURE_REFERENCES.writeRadius = 0;
-+ ChunkStatus.BIOMES.writeRadius = 0;
-+ ChunkStatus.NOISE.writeRadius = 0;
-+ ChunkStatus.SURFACE.writeRadius = 0;
-+ ChunkStatus.CARVERS.writeRadius = 0;
-+ ChunkStatus.FEATURES.writeRadius = 1;
-+ ChunkStatus.INITIALIZE_LIGHT.writeRadius = 0;
-+ ChunkStatus.LIGHT.writeRadius = 2;
-+ ChunkStatus.SPAWN.writeRadius = 0;
-+ ChunkStatus.FULL.writeRadius = 0;
++ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1);
++ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2);
++ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0);
++ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0);
++
++ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true);
++ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true);
+
+ /*
+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as
@@ -8108,26 +10286,31 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ );
+
+ for (final ChunkStatus status : parallelCapableStatus) {
-+ status.isParallelCapable = true;
++ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true);
+ }
+ }
+
++ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()];
++ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()];
+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()];
-+ private static final int[] MAX_ACCESS_RADIUS_TABLE = new int[ACCESS_RADIUS_TABLE.length];
+ static {
++ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1);
++ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1);
+ Arrays.fill(ACCESS_RADIUS_TABLE, -1);
+ }
+
-+ private static int getAccessRadius0(final ChunkStatus genStatus) {
-+ if (genStatus == ChunkStatus.EMPTY) {
++ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) {
++ if (toStatus == ChunkStatus.EMPTY) {
+ return 0;
+ }
+
-+ final int radius = Math.max(genStatus.loadRange, genStatus.getRange());
++ final ChunkStep chunkStep = pyramid.getStepTo(toStatus);
++
++ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY);
+ int maxRange = radius;
+
-+ for (int dist = 1; dist <= radius; ++dist) {
-+ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(genStatus, radius);
++ for (int dist = 0; dist <= radius; ++dist) {
++ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist);
+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()];
+ if (rad == -1) {
+ throw new IllegalStateException();
@@ -8139,22 +10322,24 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return maxRange;
+ }
+
-+ private static int maxAccessRadius;
++ private static final int MAX_ACCESS_RADIUS;
+
+ static {
+ final List<ChunkStatus> statuses = ChunkStatus.getStatusList();
+ for (int i = 0, len = statuses.size(); i < len; ++i) {
-+ ACCESS_RADIUS_TABLE[i] = getAccessRadius0(statuses.get(i));
-+ }
-+ int max = 0;
-+ for (int i = 0, len = statuses.size(); i < len; ++i) {
-+ MAX_ACCESS_RADIUS_TABLE[i] = max = Math.max(ACCESS_RADIUS_TABLE[i], max);
++ final ChunkStatus status = statuses.get(i);
++ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID);
++ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID);
++ ACCESS_RADIUS_TABLE[i] = Math.max(
++ ACCESS_RADIUS_TABLE_LOAD[i],
++ ACCESS_RADIUS_TABLE_GEN[i]
++ );
+ }
-+ maxAccessRadius = max;
++ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1];
+ }
+
+ public static int getMaxAccessRadius() {
-+ return maxAccessRadius;
++ return MAX_ACCESS_RADIUS;
+ }
+
+ public static int getAccessRadius(final ChunkStatus genStatus) {
@@ -8165,13 +10350,13 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL);
+ }
+
-+ final ReentrantAreaLock schedulingLockArea;
++
++ public final ReentrantAreaLock schedulingLockArea;
+ private final int lockShift;
+
+ public final int getChunkSystemLockShift() {
+ return this.lockShift;
+ }
-+ // Folia end - use area based lock to reduce contention
+
+ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) {
+ this.world = world;
@@ -8181,15 +10366,14 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning
+ // the entire section
+ // we just take the max, as we want the smallest shift that satisfies these properties
-+ this.lockShift = Math.max(world.getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
++ this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
+ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift());
+
-+ final String worldName = world.getWorld().getName();
-+ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", Math.max(1, newChunkSystemGenParallelism));
-+ this.radiusAwareGenExecutor =
-+ newChunkSystemGenParallelism <= 1 ? this.parallelGenExecutor : workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", newChunkSystemGenParallelism);
-+ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", newChunkSystemLoadParallelism);
-+ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenParallelism));
++ final String worldName = WorldUtil.getWorldName(world);
++ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism));
++ this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism));
++ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism);
++ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism));
+ this.chunkHolderManager = new ChunkHolderManager(world, this);
+ }
+
@@ -8230,11 +10414,11 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
+ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
+ // so, make the main thread pick it up
-+ MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
++ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
+ }
+
+ public boolean executeMainThreadTask() {
-+ TickThread.ensureTickThread("Cannot execute main thread task off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot execute main thread task off-main");
+ return this.mainThreadExecutor.executeTask();
+ }
+
@@ -8250,12 +10434,10 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ this.chunkHolderManager.lowerPriority(x, z, priority);
+ }
+
-+ private final AtomicLong chunkLoadCounter = new AtomicLong();
-+
+ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus,
+ final boolean addTicket, final PrioritisedExecutor.Priority priority,
+ final Consumer<LevelChunk> onComplete) {
-+ if (!TickThread.isTickThread()) {
++ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
@@ -8274,11 +10456,11 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ }
+
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
-+ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null;
++ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
-+ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference);
++ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
@@ -8289,10 +10471,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ }
+ } finally {
+ if (addTicket) {
-+ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey,
-+ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey),
-+ TicketType.CHUNK_LOAD, minLevel, chunkReference
-+ );
++ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
@@ -8342,8 +10521,6 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk full status callback", thr);
+ }
@@ -8360,7 +10537,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ if (chunk == null) {
+ onComplete.accept(null);
+ } else {
-+ if (chunk.getStatus().isOrAfter(toStatus)) {
++ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ onComplete.accept(null);
@@ -8369,16 +10546,16 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ });
+ }
+
-+ // only appropriate to use with ServerLevel#syncLoadNonFull
++ // only appropriate to use with syncLoadNonFull
+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final PrioritisedExecutor.Priority priority) {
+ final int accessRadius = getAccessRadius(toStatus);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
++ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
-+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
-+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
@@ -8409,9 +10586,45 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return true;
+ }
+
++ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only
++ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock
++ // between regions
++ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
++ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
++ throw new IllegalArgumentException("Status: " + status);
++ }
++ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
++ if (loaded != null) {
++ return loaded;
++ }
++
++ final Long ticketId = getNextNonFullLoadId();
++ final int ticketLevel = getTicketLevel(status);
++ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
++ this.chunkHolderManager.processTicketUpdates();
++
++ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING);
++
++ // we could do a simple spinwait here, since we do not need to process tasks while performing this load
++ // but we process tasks only because it's a better use of the time spent
++ this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> {
++ return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null;
++ });
++
++ loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
++
++ this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
++
++ if (loaded == null) {
++ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
++ }
++
++ return loaded;
++ }
++
+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
+ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ if (!TickThread.isTickThread()) {
++ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
+ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
@@ -8430,12 +10643,12 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return;
+ }
+
-+ final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
-+ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null;
++ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
++ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
-+ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference);
++ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
@@ -8446,10 +10659,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ }
+ } finally {
+ if (addTicket) {
-+ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey,
-+ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey),
-+ TicketType.CHUNK_LOAD, minLevel, chunkReference
-+ );
++ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
@@ -8497,8 +10707,6 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
@@ -8506,7 +10714,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ }
+
+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk,
-+ final NewChunkHolder chunkHolder, final List<ChunkAccess> neighbours,
++ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours,
+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
@@ -8523,7 +10731,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+
+ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder,
+ final List<ChunkProgressionTask> allTasks) {
-+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority());
++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL));
+ }
+
+ // rets new task scheduled for the _specified_ chunk
@@ -8542,14 +10750,16 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return null;
+ }
+
-+ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(minPriority, chunkHolder.getEffectivePriority());
++ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(
++ minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ );
+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
+ final ChunkAccess chunk = chunkHolder.getCurrentChunk();
+
+ if (currentGenStatus == null) {
+ // not yet loaded
+ final ChunkProgressionTask task = this.createTask(
-+ chunkX, chunkZ, chunk, chunkHolder, Collections.emptyList(), ChunkStatus.EMPTY, requestedPriority
++ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority
+ );
+
+ allTasks.add(task);
@@ -8571,37 +10781,29 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ // we know for sure now that we want to schedule _something_, so set the target
+ chunkHolder.setGenerationTarget(targetStatus);
+
-+ final ChunkStatus chunkRealStatus = chunk.getStatus();
-+ final ChunkStatus toStatus = currentGenStatus.getNextStatus();
++ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus();
++ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus();
++ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID;
++ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus);
+
-+ // if this chunk has already generated up to or past the specified status, then we don't
-+ // need the neighbours AT ALL.
-+ final int neighbourReadRadius = chunkRealStatus.isOrAfter(toStatus) ? toStatus.loadRange : toStatus.getRange();
++ final int neighbourReadRadius = Math.max(
++ 0,
++ chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
++ );
+
+ boolean unGeneratedNeighbours = false;
+
-+ // copied from MCUtil.getSpiralOutChunks
-+ for (int r = 1; r <= neighbourReadRadius; r++) {
-+ int x = -r;
-+ int z = r;
-+
-+ // Iterates the edge of half of the box; then negates for other half.
-+ while (x <= r && z > -r) {
++ if (neighbourReadRadius > 0) {
++ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap;
++ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) {
++ final int x = CoordinateUtils.getChunkX(pos);
++ final int z = CoordinateUtils.getChunkZ(pos);
+ final int radius = Math.max(Math.abs(x), Math.abs(z));
-+ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(toStatus, radius);
++ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius);
+
+ unGeneratedNeighbours |= this.checkNeighbour(
-+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
-+ );
-+ unGeneratedNeighbours |= this.checkNeighbour(
-+ chunkX - x, chunkZ - z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
++ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
+ );
-+
-+ if (x < r) {
-+ x++;
-+ } else {
-+ z--;
-+ }
+ }
+ }
+
@@ -8615,28 +10817,19 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+
+ // need to gather neighbours
+
-+ final List<ChunkAccess> neighbours;
-+ final List<NewChunkHolder> chunkHolderNeighbours;
-+ if (neighbourReadRadius <= 0) {
-+ neighbours = new ArrayList<>(1);
-+ chunkHolderNeighbours = new ArrayList<>(1);
-+ neighbours.add(chunk);
-+ chunkHolderNeighbours.add(chunkHolder);
-+ } else {
-+ // the iteration order is _very_ important, as all generation statuses expect a certain order such that:
-+ // chunkAtRelative = neighbours.get(relX + relZ * (2 * radius + 1))
-+ neighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
-+ chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
-+ for (int dz = -neighbourReadRadius; dz <= neighbourReadRadius; ++dz) {
-+ for (int dx = -neighbourReadRadius; dx <= neighbourReadRadius; ++dx) {
-+ final NewChunkHolder holder = (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ);
-+ neighbours.add(holder.getChunkForNeighbourAccess());
++ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
++ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D
++ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> {
++ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz);
+ chunkHolderNeighbours.add(holder);
-+ }
-+ }
-+ }
+
-+ final ChunkProgressionTask task = this.createTask(chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, chunkHolder.getEffectivePriority());
++ return holder.vanillaChunkHolder;
++ });
++
++ final ChunkProgressionTask task = this.createTask(
++ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
++ chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ );
+ allTasks.add(task);
+
+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours);
@@ -8711,17 +10904,6 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ return this.mainThreadExecutor.queueRunnable(run, priority);
+ }
+
-+ public void executeTasksUntil(final BooleanSupplier exit) {
-+ if (Bukkit.isPrimaryThread()) {
-+ this.mainThreadExecutor.executeConditionally(exit);
-+ } else {
-+ long counter = 1L;
-+ while (!exit.getAsBoolean()) {
-+ counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms
-+ }
-+ }
-+ }
-+
+ public boolean halt(final boolean sync, final long maxWaitNS) {
+ this.radiusAwareGenExecutor.halt();
+ this.parallelGenExecutor.halt();
@@ -8761,7 +10943,7 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+
+ @Override
+ public String toString() {
-+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']";
++ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']";
+ }
+ }
+
@@ -8783,16 +10965,19 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ }
+ }
+
++ // Paper start
+ public static void dumpAllChunkLoadInfo(final boolean longPrint) {
+ final ChunkInfo[] chunkInfos = getChunkInfos();
+ if (chunkInfos.length > 0) {
+ LOGGER.error("Chunk wait task info below: ");
+ for (final ChunkInfo chunkInfo : chunkInfos) {
-+ final NewChunkHolder holder = chunkInfo.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
++ final NewChunkHolder holder = chunkInfo.world.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
+ LOGGER.error("Chunk wait: " + chunkInfo);
+ LOGGER.error("Chunk holder: " + holder);
+ }
+
++ // TODO
++ /*
+ if (longPrint) {
+ final File file = new File(new File(new File("."), "debug"), "chunks-watchdog.txt");
+ LOGGER.error("Writing chunk information dump to " + file);
@@ -8800,1009 +10985,47 @@ index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f
+ MCUtil.dumpChunks(file, true);
+ LOGGER.error("Successfully written chunk information!");
+ } catch (final Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..bd0d0c4436f357392e13d9efd4412886385a6924
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java
-@@ -0,0 +1,214 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ChunkResult;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.status.WorldGenContext;
-+import org.slf4j.Logger;
-+import java.lang.invoke.VarHandle;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.concurrent.CompletableFuture;
-+
-+public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ protected final ChunkAccess fromChunk;
-+ protected final ChunkStatus fromStatus;
-+ protected final ChunkStatus toStatus;
-+ protected final List<ChunkAccess> neighbours;
-+
-+ protected final PrioritisedExecutor.PrioritisedTask generateTask;
-+
-+ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final ChunkAccess chunk, final List<ChunkAccess> neighbours,
-+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.fromChunk = chunk;
-+ this.fromStatus = chunk.getStatus();
-+ this.toStatus = toStatus;
-+ this.neighbours = neighbours;
-+ if (this.toStatus.isParallelCapable) {
-+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority);
-+ } else {
-+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, this.toStatus.writeRadius, this, priority);
-+ }
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return this.toStatus;
-+ }
-+
-+ private boolean isEmptyTask() {
-+ // must use fromStatus here to avoid any race condition with run() overwriting the status
-+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus);
-+ return (generation && this.toStatus.isEmptyGenStatus()) || (!generation && this.toStatus.isEmptyLoadStatus());
-+ }
-+
-+ @Override
-+ public void run() {
-+ final ChunkAccess chunk = this.fromChunk;
-+
-+ final ServerChunkCache serverChunkCache = this.world.chunkSource;
-+ final ChunkMap chunkMap = serverChunkCache.chunkMap;
-+
-+ final CompletableFuture<ChunkAccess> completeFuture;
-+
-+ final boolean generation;
-+ boolean completing = false;
-+
-+ // note: should optimise the case where the chunk does not need to execute the status, because
-+ // schedule() calls this synchronously if it will run through that path
-+
-+ final WorldGenContext ctx = new WorldGenContext(
-+ this.world,
-+ chunkMap.generator,
-+ chunkMap.getWorldGenContext().structureManager(),
-+ serverChunkCache.getLightEngine()
-+ );
-+ try {
-+ generation = !chunk.getStatus().isOrAfter(this.toStatus);
-+ if (generation) {
-+ if (this.toStatus.isEmptyGenStatus()) {
-+ if (chunk instanceof ProtoChunk) {
-+ ((ProtoChunk)chunk).setStatus(this.toStatus);
-+ }
-+ completing = true;
-+ this.complete(chunk, null);
-+ return;
-+ }
-+ completeFuture = this.toStatus.generate(ctx, Runnable::run, null, this.neighbours)
-+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> {
-+ if (either instanceof ProtoChunk proto) {
-+ proto.setStatus(ChunkUpgradeGenericStatusTask.this.toStatus);
-+ }
-+ }
-+ );
-+ } else {
-+ if (this.toStatus.isEmptyLoadStatus()) {
-+ completing = true;
-+ this.complete(chunk, null);
-+ return;
-+ }
-+ completeFuture = this.toStatus.load(ctx, null, chunk);
-+ }
-+ } catch (final Throwable throwable) {
-+ if (!completing) {
-+ this.complete(null, throwable);
-+
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ return;
-+ }
-+
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus),
-+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus),
-+ "Generation task", this
-+ ), throwable);
-+
-+ if (!(throwable instanceof ThreadDeath)) {
-+ LOGGER.error("Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), world: " + this.world.getWorld().getName(), throwable);
-+ } else {
-+ // ensure the chunk system can respond, then die
-+ throw (ThreadDeath)throwable;
-+ }
-+ return;
-+ }
-+
-+ if (!completeFuture.isDone() && !this.toStatus.warnedAboutNoImmediateComplete.getAndSet(true)) {
-+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation);
-+ }
-+
-+ final ChunkAccess newChunk;
-+
-+ try {
-+ newChunk = completeFuture.join();
-+ } catch (final Throwable throwable) {
-+ this.complete(null, throwable);
-+ // ensure the chunk system can respond, then die
-+ if (throwable instanceof ThreadDeath) {
-+ throw (ThreadDeath)throwable;
-+ }
-+ return;
-+ }
-+
-+ if (newChunk == null) {
-+ this.complete(null, new IllegalStateException("Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + ", generation: " + generation + " should not be null! Future: " + completeFuture).fillInStackTrace());
-+ return;
-+ }
-+
-+ this.complete(newChunk, null);
-+ }
-+
-+ protected volatile boolean scheduled;
-+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class);
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) {
-+ throw new IllegalStateException("Cannot double call schedule()");
-+ }
-+ if (this.isEmptyTask()) {
-+ if (this.generateTask.cancel()) {
-+ this.run();
-+ }
-+ } else {
-+ this.generateTask.queue();
-+ }
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ if (this.generateTask.cancel()) {
-+ this.complete(null, null);
-+ }
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
-+ return this.generateTask.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.lowerPriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.raisePriority(priority);
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..396d72c00e47cf1669ae20dc839c1c961b1f262a
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java
-@@ -0,0 +1,746 @@
-+package io.papermc.paper.chunk.system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.completable.Completable;
-+import ca.spottedleaf.concurrentutil.executor.Cancellable;
-+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import org.slf4j.Logger;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.BiConsumer;
-+
-+public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag();
-+
-+ // reference count is the upper 32 bits
-+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED);
-+
-+ protected static final long STAGE_MASK = 0xFFFFFFFFL;
-+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL;
-+ protected static final long STAGE_NOT_STARTED = 0L;
-+ protected static final long STAGE_LOADING = 1L;
-+ protected static final long STAGE_PROCESSING = 2L;
-+ protected static final long STAGE_COMPLETED = 3L;
-+
-+ // for loading data off disk
-+ protected final LoadDataFromDiskTask loadDataFromDiskTask;
-+ // processing off-main
-+ protected final PrioritisedExecutor.PrioritisedTask processOffMain;
-+ // processing on-main
-+ protected final PrioritisedExecutor.PrioritisedTask processOnMain;
-+
-+ protected final ChunkTaskScheduler scheduler;
-+ protected final ServerLevel world;
-+ protected final int chunkX;
-+ protected final int chunkZ;
-+ protected final RegionFileIOThread.RegionFileType type;
-+
-+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
-+ this.scheduler = scheduler;
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.type = type;
-+
-+ final ProcessOnMainTask mainTask;
-+ if (this.hasOnMain()) {
-+ mainTask = new ProcessOnMainTask();
-+ this.processOnMain = this.createOnMain(mainTask, priority);
-+ } else {
-+ mainTask = null;
-+ this.processOnMain = null;
-+ }
-+
-+ final ProcessOffMainTask offMainTask;
-+ if (this.hasOffMain()) {
-+ offMainTask = new ProcessOffMainTask(mainTask);
-+ this.processOffMain = this.createOffMain(offMainTask, priority);
-+ } else {
-+ offMainTask = null;
-+ this.processOffMain = null;
-+ }
-+
-+ if (this.processOffMain == null && this.processOnMain == null) {
-+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!");
-+ }
-+
-+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority);
-+ }
-+
-+ public static final record TaskResult<L, R>(L left, R right) {}
-+
-+ protected abstract boolean hasOffMain();
-+
-+ protected abstract boolean hasOnMain();
-+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
-+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority);
-+
-+ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable);
-+
-+ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable);
-+
-+ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result);
-+
-+ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable);
-+
-+ @Override
-+ public String toString() {
-+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + this.world.getWorld().getName() +
-+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
-+ ", type: " + this.type.toString() + "}";
-+ }
-+
-+ public PrioritisedExecutor.Priority getPriority() {
-+ if (this.processOnMain != null) {
-+ return this.processOnMain.getPriority();
-+ } else {
-+ return this.processOffMain.getPriority();
-+ }
-+ }
-+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ if (this.processOffMain != null) {
-+ this.processOffMain.lowerPriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.lowerPriority(priority);
-+ }
-+ }
-+
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ this.loadDataFromDiskTask.raisePriority(priority);
-+ if (this.processOffMain != null) {
-+ this.processOffMain.setPriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.setPriority(priority);
-+ }
-+ }
-+
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ this.loadDataFromDiskTask.raisePriority(priority);
-+ if (this.processOffMain != null) {
-+ this.processOffMain.raisePriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.raisePriority(priority);
-+ }
-+ }
-+
-+ // returns whether scheduleNow() needs to be called
-+ public boolean schedule(final boolean delay) {
-+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED ||
-+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) {
-+ // try and increment reference count
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) {
-+ // cancelled or completed, nothing to do here
-+ return false;
-+ }
-+
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) {
-+ // successful
-+ return false;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ if (!delay) {
-+ this.scheduleNow();
-+ return false;
-+ }
-+ return true;
-+ }
-+
-+ public void scheduleNow() {
-+ this.loadDataFromDiskTask.schedule(); // will schedule the rest
-+ }
-+
-+ // assumes the current stage cannot be completed
-+ // returns false if cancelled, returns true if can proceed
-+ private boolean advanceStage(final long expect, final long to) {
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) != expect) {
-+ // must be cancelled
-+ return false;
-+ }
-+
-+ final long newVal = (curr & ~STAGE_MASK) | to;
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ return true;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public boolean cancel() {
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) {
-+ return false;
-+ }
-+
-+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) {
-+ // no other references, so we can cancel
-+ final long newVal = STAGE_CANCELLED;
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ this.loadDataFromDiskTask.cancel();
-+ if (this.processOffMain != null) {
-+ this.processOffMain.cancel();
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.cancel();
-+ }
-+ this.onComplete(null);
-+ return true;
-+ }
-+ } else {
-+ if ((curr & ~STAGE_MASK) == (0L << 32)) {
-+ throw new IllegalStateException("Reference count cannot be zero here");
-+ }
-+ // just decrease the reference count
-+ final long newVal = curr - (1L << 32);
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ return false;
-+ }
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ protected final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> {
-+
-+ protected final ProcessOffMainTask offMainTask;
-+ protected final ProcessOnMainTask onMainTask;
-+
-+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) {
-+ this.offMainTask = offMainTask;
-+ this.onMainTask = onMainTask;
-+ }
-+
-+ @Override
-+ public void accept(final CompoundTag compoundTag, final Throwable throwable) {
-+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
-+ // don't try to schedule further
-+ return;
-+ }
-+
-+ try {
-+ if (compoundTag == CANCELLED_DATA) {
-+ // cancelled, except this isn't possible
-+ LOGGER.error("Data callback says cancelled, but stage does not?");
-+ return;
-+ }
-+
-+ // get off of the regionfile callback ASAP, no clue what locks are held right now...
-+ if (GenericDataLoadTask.this.processOffMain != null) {
-+ this.offMainTask.data = compoundTag;
-+ this.offMainTask.throwable = throwable;
-+ GenericDataLoadTask.this.processOffMain.queue();
-+ return;
-+ } else {
-+ // no off-main task, so go straight to main
-+ this.onMainTask.data = (OnMain)compoundTag;
-+ this.onMainTask.throwable = throwable;
-+ GenericDataLoadTask.this.processOnMain.queue();
-+ }
-+ } catch (final ThreadDeath death) {
-+ throw death;
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2);
-+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure(
-+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of(
-+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable)
-+ ), thr2);
-+ }
-+ }
-+ }
-+
-+ protected final class ProcessOffMainTask implements Runnable {
-+
-+ protected CompoundTag data;
-+ protected Throwable throwable;
-+ protected final ProcessOnMainTask schedule;
-+
-+ public ProcessOffMainTask(final ProcessOnMainTask schedule) {
-+ this.schedule = schedule;
-+ }
-+
-+ @Override
-+ public void run() {
-+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) {
-+ // cancelled
-+ return;
-+ }
-+ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable);
-+
-+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
-+ // don't try to schedule further
-+ return;
-+ }
-+
-+ if (this.schedule != null) {
-+ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right);
-+
-+ if (syncComplete != null) {
-+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
-+ GenericDataLoadTask.this.onComplete(syncComplete);
-+ } // else: cancelled
-+ return;
-+ }
-+
-+ this.schedule.data = newData.left;
-+ this.schedule.throwable = newData.right;
-+
-+ GenericDataLoadTask.this.processOnMain.queue();
-+ } else {
-+ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData);
-+ }
-+ }
-+ }
-+
-+ protected final class ProcessOnMainTask implements Runnable {
-+
-+ protected OnMain data;
-+ protected Throwable throwable;
-+
-+ @Override
-+ public void run() {
-+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
-+ // cancelled
-+ return;
-+ }
-+ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable);
-+
-+ GenericDataLoadTask.this.onComplete(result);
-+ }
-+ }
-+
-+ public static final class LoadDataFromDiskTask {
-+
-+ protected volatile int priority;
-+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class);
-+
-+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0;
-+ protected static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1;
-+ protected static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2;
-+
-+ protected static final int PRIORITY_FLAGS = ~Character.MAX_VALUE;
-+
-+ protected final int getPriorityVolatile() {
-+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this);
-+ }
-+
-+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
-+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update);
-+ }
-+
-+ protected final int getAndOrPriorityVolatile(final int val) {
-+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val);
-+ }
-+
-+ protected final void setPriorityPlain(final int val) {
-+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val);
-+ }
-+
-+ private final ServerLevel world;
-+ private final int chunkX;
-+ private final int chunkZ;
-+
-+ private final RegionFileIOThread.RegionFileType type;
-+ private Cancellable dataLoadTask;
-+ private Cancellable dataUnloadCancellable;
-+ private DelayedPrioritisedTask dataUnloadTask;
-+
-+ private final BiConsumer<CompoundTag, Throwable> onComplete;
-+
-+ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does
-+ // hold a priority lock.
-+ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final RegionFileIOThread.RegionFileType type,
-+ final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.type = type;
-+ this.onComplete = onComplete;
-+ this.setPriorityPlain(priority.priority);
-+ }
-+
-+ private void complete(final CompoundTag data, final Throwable throwable) {
-+ try {
-+ this.onComplete.accept(data, throwable);
-+ } catch (final Throwable thr2) {
-+ this.world.chunkTaskScheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable),
-+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type)
-+ ), thr2);
-+ if (thr2 instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr2;
-+ }
-+ }
-+ }
-+
-+ protected boolean markExecuting() {
-+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
-+ }
-+
-+ protected boolean isMarkedExecuted() {
-+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
-+ }
-+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.lowerPriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.setPriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.raisePriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void cancel() {
-+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed already
-+ return;
-+ }
-+
-+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
-+ // the write to dataLoadTask will check for the cancelled bit
-+ if (this.dataUnloadCancellable != null) {
-+ this.dataUnloadCancellable.cancel();
-+ }
-+
-+ if (this.dataLoadTask != null) {
-+ this.dataLoadTask.cancel();
-+ }
-+
-+ this.complete(CANCELLED_DATA, null);
-+ }
-+
-+ private final AtomicBoolean scheduled = new AtomicBoolean();
-+
-+ public void schedule() {
-+ if (this.scheduled.getAndSet(true)) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+ int priority = this.getPriorityVolatile();
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled
-+ return;
-+ }
-+
-+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
-+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
-+ // to mark complete here to ensure we do not double complete
-+ if (LoadDataFromDiskTask.this.markExecuting()) {
-+ LoadDataFromDiskTask.this.complete(data, thr);
-+ } // else: cancelled
-+ };
-+
-+ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
-+ boolean scheduledUnload = false;
-+
-+ final NewChunkHolder holder = this.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
-+ if (holder != null) {
-+ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> {
-+ if (data != null) {
-+ consumer.accept(data, null);
-+ } else {
-+ // need to schedule task
-+ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
-+ }
-+ };
-+ Cancellable unloadCancellable = null;
-+ CompoundTag syncComplete = null;
-+ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists
-+ final Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
-+ if (unloadCompletable != null) {
-+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
-+ if (unloadCancellable == null) {
-+ syncComplete = unloadCompletable.getResult();
-+ }
-+ }
-+
-+ if (syncComplete != null) {
-+ consumer.accept(syncComplete, null);
-+ return;
-+ }
-+
-+ if (unloadCancellable != null) {
-+ scheduledUnload = true;
-+ this.dataUnloadCancellable = unloadCancellable;
-+ this.dataUnloadTask = unloadTask.task();
++ LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ }
+ }
-+
-+ this.schedule(scheduledUnload, consumer, initialPriority);
++ */
+ }
-+
-+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
-+ int priority = this.getPriorityVolatile();
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled
-+ return;
-+ }
-+
-+ if (!scheduledUnload) {
-+ this.dataLoadTask = RegionFileIOThread.loadDataAsync(
-+ this.world, this.chunkX, this.chunkZ, this.type, consumer,
-+ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
-+ );
-+ }
-+
-+ int failures = 0;
-+ for (;;) {
-+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) {
-+ return;
-+ }
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ if (this.dataUnloadCancellable != null) {
-+ this.dataUnloadCancellable.cancel();
-+ }
-+
-+ if (this.dataLoadTask != null) {
-+ this.dataLoadTask.cancel();
-+ }
-+ return;
-+ }
-+
-+ if (scheduledUnload) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ }
-+ } else {
-+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ /*
-+ private static final class LoadDataPriorityHolder extends PriorityHolder {
-+
-+ protected final LoadDataFromDiskTask task;
-+
-+ protected LoadDataPriorityHolder(final PrioritisedExecutor.Priority priority, final LoadDataFromDiskTask task) {
-+ super(priority);
-+ this.task = task;
-+ }
-+
-+ @Override
-+ protected void cancelScheduled() {
-+ final Cancellable dataLoadTask = this.task.dataLoadTask;
-+ if (dataLoadTask != null) {
-+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
-+ // the write to dataLoadTask will check for the cancelled bit
-+ this.task.dataLoadTask.cancel();
-+ }
-+ this.task.complete(CANCELLED_DATA, null);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.Priority getScheduledPriority() {
-+ final LoadDataFromDiskTask task = this.task;
-+ return RegionFileIOThread.getPriority(task.world, task.chunkX, task.chunkZ, task.type);
-+ }
-+
-+ @Override
-+ protected void scheduleTask(final PrioritisedExecutor.Priority priority) {
-+ final LoadDataFromDiskTask task = this.task;
-+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
-+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
-+ // to mark complete here to ensure we do not double complete
-+ if (LoadDataPriorityHolder.this.markExecuting()) {
-+ LoadDataPriorityHolder.this.task.complete(data, thr);
-+ } // else: cancelled
-+ };
-+ task.dataLoadTask = RegionFileIOThread.loadDataAsync(
-+ task.world, task.chunkX, task.chunkZ, task.type, consumer,
-+ priority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), priority
-+ );
-+ if (this.isMarkedExecuted()) {
-+ // if we are marked as completed, it could be:
-+ // 1. we were cancelled
-+ // 2. the consumer was completed
-+ // in the 2nd case, cancel() does nothing
-+ // in the 1st case, we ensure cancel() is called as it is possible for the cancelling thread
-+ // to miss the field write here
-+ task.dataLoadTask.cancel();
-+ }
-+ }
-+
-+ @Override
-+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final LoadDataFromDiskTask task = this.task;
-+ RegionFileIOThread.lowerPriority(task.world, task.chunkX, task.chunkZ, task.type, priority);
-+ }
-+
-+ @Override
-+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final LoadDataFromDiskTask task = this.task;
-+ RegionFileIOThread.setPriority(task.world, task.chunkX, task.chunkZ, task.type, priority);
-+ }
-+
-+ @Override
-+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ final LoadDataFromDiskTask task = this.task;
-+ RegionFileIOThread.raisePriority(task.world, task.chunkX, task.chunkZ, task.type, priority);
-+ }
-+ }
-+ */
+ }
++ // Paper end
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
new file mode 100644
-index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb0db6379a
+index 0000000000000000000000000000000000000000..dd86394b4503dc47c17517b2c79e482fa683b3cf
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
-@@ -0,0 +1,2106 @@
-+package io.papermc.paper.chunk.system.scheduling;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
+@@ -0,0 +1,2014 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.completable.Completable;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures;
++import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
++import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.chunk.system.io.RegionFileIOThread;
-+import io.papermc.paper.chunk.system.poi.PoiChunk;
-+import io.papermc.paper.util.CoordinateUtils;
-+import io.papermc.paper.util.TickThread;
-+import io.papermc.paper.util.WorldUtil;
-+import io.papermc.paper.world.ChunkEntitySlices;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
@@ -9812,17 +11035,15 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import org.slf4j.Logger;
-+import java.lang.invoke.VarHandle;
++import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
@@ -9833,16 +11054,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+public final class NewChunkHolder {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ public static final Thread.UncaughtExceptionHandler CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() {
-+ @Override
-+ public void uncaughtException(final Thread thread, final Throwable throwable) {
-+ if (!(throwable instanceof ThreadDeath)) {
-+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
-+ }
-+ }
-+ };
++ private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
@@ -9859,7 +11071,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ private CompoundTag pendingEntityChunk;
+
+ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
+ final CompoundTag entityChunk;
+ final ChunkEntitySlices ret;
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
@@ -9880,7 +11092,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ ret.setTransient(transientChunk);
+
-+ this.world.getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret);
++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret);
+ } else {
+ // transientChunk = false here
+ ret = this.entityChunk;
@@ -9899,9 +11111,9 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ if (!transientChunk) {
+ if (entityChunk != null) {
-+ final List<Entity> entities = EntityStorage.readEntities(this.world, entityChunk);
++ final List<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk);
+
-+ this.world.getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ));
++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ));
+ }
+ }
+
@@ -9959,7 +11171,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // no tasks to schedule _for_
+ } else {
+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority()
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ entityDataLoadTask.addCallback(this::completeEntityLoad);
+ // need one schedule() per waiter
@@ -10006,7 +11218,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ if (this.entityDataLoadTask == null) {
+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority()
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.entityDataLoadTask.addCallback(this::completeEntityLoad);
+ this.entityDataLoadTaskWaiters = new ArrayList<>();
@@ -10080,7 +11292,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // no tasks to schedule _for_
+ } else {
+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority()
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ poiDataLoadTask.addCallback(this::completePoiLoad);
+ // need one schedule() per waiter
@@ -10126,7 +11338,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ if (this.poiDataLoadTask == null) {
+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority()
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.poiDataLoadTask.addCallback(this::completePoiLoad);
+ this.poiDataLoadTaskWaiters = new ArrayList<>();
@@ -10214,7 +11426,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ @Override
+ public boolean cancel() {
-+ final NewChunkHolder holder = this.chunkHolder; // Folia - use area based lock to reduce contention
++ final NewChunkHolder holder = this.chunkHolder;
+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ);
+ try {
+ if (!this.completed) {
@@ -10258,12 +11470,12 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ /**
+ * contains the neighbours that this chunk generation is blocking on
+ */
-+ protected final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4);
++ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4);
+
+ /**
+ * map of ChunkHolder -> Required Status for this chunk
+ */
-+ protected final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>();
++ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>();
+
+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) {
+ this.neighboursBlockingGenTask.add(neighbour);
@@ -10280,35 +11492,44 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // priority state
+
+ // the target priority for this chunk to generate at
-+ // TODO this will screw over scheduling at lower priorities to neighbours, fix
-+ private PrioritisedExecutor.Priority priority = PrioritisedExecutor.Priority.NORMAL;
++ private PrioritisedExecutor.Priority priority = null;
+ private boolean priorityLocked;
+
+ // the priority neighbouring chunks have requested this chunk generate at
-+ private PrioritisedExecutor.Priority neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE;
++ private PrioritisedExecutor.Priority neighbourRequestedPriority = null;
++
++ public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) {
++ final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority;
++ final PrioritisedExecutor.Priority us = this.priority;
++
++ if (neighbour == null) {
++ return us == null ? dfl : us;
++ }
++ if (us == null) {
++ return dfl;
++ }
+
-+ public PrioritisedExecutor.Priority getEffectivePriority() {
-+ return PrioritisedExecutor.Priority.max(this.priority, this.neighbourRequestedPriority);
++ return PrioritisedExecutor.Priority.max(us, neighbour);
+ }
+
-+ protected void recalculateNeighbourRequestedPriority() {
++ private void recalculateNeighbourRequestedPriority() {
+ if (this.neighboursWaitingForUs.isEmpty()) {
-+ this.neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE;
++ this.neighbourRequestedPriority = null;
+ return;
+ }
+
-+ PrioritisedExecutor.Priority max = PrioritisedExecutor.Priority.IDLE;
++ PrioritisedExecutor.Priority max = null;
+
+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
-+ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority();
-+ if (neighbourPriority.isHigherPriority(max)) {
++ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null);
++ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
+ max = neighbourPriority;
+ }
+ }
+
-+ final PrioritisedExecutor.Priority current = this.getEffectivePriority();
++ final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+ this.neighbourRequestedPriority = max;
-+ final PrioritisedExecutor.Priority next = this.getEffectivePriority();
++ final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (current == next) {
+ return;
@@ -10331,14 +11552,14 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ // must hold scheduling lock
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) {
++ if (this.priority == null || this.priority.isHigherOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
+ }
+
+ private void lockPriority() {
-+ this.priority = PrioritisedExecutor.Priority.NORMAL;
++ this.priority = null;
+ this.priorityLocked = true;
+ }
+
@@ -10347,9 +11568,9 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ if (this.priorityLocked) {
+ return;
+ }
-+ final PrioritisedExecutor.Priority old = this.getEffectivePriority();
++ final PrioritisedExecutor.Priority old = this.getEffectivePriority(null);
+ this.priority = priority;
-+ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority();
++ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (old != newPriority) {
+ if (this.generationTask != null) {
@@ -10362,7 +11583,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ // must hold scheduling lock
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) {
++ if (this.priority == null || this.priority.isLowerOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
@@ -10384,8 +11605,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ // ticket level state
-+ private int oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
-+ private int currentTicketLevel = ChunkLevel.MAX_LEVEL + 1;
++ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
++ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+
+ public int getTicketLevel() {
+ return this.currentTicketLevel;
@@ -10398,10 +11619,14 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.scheduler = scheduler;
-+ this.vanillaChunkHolder = new ChunkHolder(new ChunkPos(chunkX, chunkZ), world, world.getLightEngine(), world.chunkSource.chunkMap, this);
++ this.vanillaChunkHolder = new ChunkHolder(
++ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world,
++ world.getLightEngine(), null, world.getChunkSource().chunkMap
++ );
++ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this);
+ }
+
-+ protected ImposterProtoChunk wrappedChunkForNeighbour;
++ private ImposterProtoChunk wrappedChunkForNeighbour;
+
+ // holds scheduling lock
+ public ChunkAccess getChunkForNeighbourAccess() {
@@ -10468,7 +11693,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ return "neighbours_waiting";
+ }
+
-+ // chunk must be marked inaccessible (i.e unloaded to plugins)
++ // chunk must be marked inaccessible (i.e. unloaded to plugins)
+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) {
+ return "fullchunkstatus";
+ }
@@ -10509,19 +11734,35 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ /** Unloaded from chunk map */
-+ boolean killed;
++ private boolean unloaded;
++
++ void markUnloaded() {
++ this.unloaded = true;
++ }
++
++ private boolean inUnloadQueue = false;
++
++ void removeFromUnloadQueue() {
++ this.inUnloadQueue = false;
++ }
+
+ // must hold scheduling lock
+ private void checkUnload() {
-+ if (this.killed) {
++ if (this.unloaded) {
+ return;
+ }
+ if (this.isSafeToUnload() == null) {
+ // ensure in unload queue
-+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ);
++ if (!this.inUnloadQueue) {
++ this.inUnloadQueue = true;
++ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ);
++ }
+ } else {
+ // ensure not in unload queue
-+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ);
++ if (this.inUnloadQueue) {
++ this.inUnloadQueue = false;
++ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ);
++ }
+ }
+ }
+
@@ -10548,15 +11789,36 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
++ private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) {
++ switch (type) {
++ case CHUNK_DATA: {
++ this.chunkDataUnload = null;
++ return;
++ }
++ case ENTITY_DATA: {
++ this.entityDataUnload = null;
++ return;
++ }
++ case POI_DATA: {
++ this.poiDataUnload = null;
++ return;
++ }
++ default:
++ throw new IllegalStateException("Unknown regionfile type " + type);
++ }
++ }
++
+ private UnloadState unloadState;
+
+ // holds schedule lock
+ UnloadState unloadStage1() {
+ // because we hold the scheduling lock, we cannot actually unload anything
-+ // so we need to null this chunk's state
-+ ChunkAccess chunk = this.currentChunk;
-+ ChunkEntitySlices entityChunk = this.entityChunk;
-+ PoiChunk poiChunk = this.poiChunk;
++ // so, what we do here instead is to null this chunk's state and setup the unload tasks
++ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which
++ // we do not hold the lock) c
++ final ChunkAccess chunk = this.currentChunk;
++ final ChunkEntitySlices entityChunk = this.entityChunk;
++ final PoiChunk poiChunk = this.poiChunk;
+ // chunk state
+ this.currentChunk = null;
+ this.currentGenStatus = null;
@@ -10586,15 +11848,16 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ // data is null if failed or does not need to be saved
-+ void completeAsyncChunkDataSave(final CompoundTag data) {
++ void completeAsyncUnloadDataSave(final RegionFileIOThread.RegionFileType type, final CompoundTag data) {
+ if (data != null) {
-+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, RegionFileIOThread.RegionFileType.CHUNK_DATA);
++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
+ }
-+ this.chunkDataUnload.completable().complete(data);
++
++ this.getUnloadTask(type).completable().complete(data);
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ // can only write to these fields while holding the schedule lock
-+ this.chunkDataUnload = null;
++ this.removeUnloadTask(type);
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
@@ -10607,7 +11870,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ final ChunkEntitySlices entityChunk = state.entityChunk();
+ final PoiChunk poiChunk = state.poiChunk();
+
-+ final boolean shouldLevelChunkNotSave = (chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave);
++ final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk);
+
+ // unload chunk data
+ if (chunk != null) {
@@ -10618,7 +11881,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ if (!shouldLevelChunkNotSave) {
+ this.saveChunk(chunk, true);
+ } else {
-+ this.completeAsyncChunkDataSave(null);
++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+
+ if (chunk instanceof LevelChunk levelChunk) {
@@ -10642,7 +11905,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } else {
-+ this.world.getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ);
++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ);
+ }
+ // we need to delay the callback until after determining transience, otherwise a potential loader could
+ // set entityChunk before we do
@@ -10658,7 +11921,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ if (poiChunk.isLoaded()) {
-+ this.world.getPoiManager().onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
++ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
+ }
+ }
+ }
@@ -10726,7 +11989,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.requestedGenStatus = null;
+ this.cancelGenTask();
+ } else {
-+ final ChunkStatus toCancel = maxGenerationStatusNew.getNextStatus();
++ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus();
+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus;
+
+ if (currentRequestedStatus.isOrAfter(toCancel)) {
@@ -10749,7 +12012,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
-+ if (newState != oldState) {
++ if (oldState != newState) {
+ if (newState.isOrAfter(oldState)) {
+ // status upgrade
+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) {
@@ -10762,9 +12025,6 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks
+ );
+ }
-+ } else {
-+ // now we are fully loaded
-+ this.queueBorderFullStatus(true, changedLoadStatus);
+ }
+ }
+ } else {
@@ -10781,10 +12041,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null);
+ }
+ }
-+ }
+
-+ if (oldState != newState) {
-+ if (this.onTicketUpdate(oldState, newState)) {
++ if (this.updatePendingStatus()) {
+ changedLoadStatus.add(this);
+ }
+ }
@@ -10794,16 +12052,6 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
-+ /*
-+ For full chunks, vanilla just loads chunks around it up to FEATURES, 1 radius
-+
-+ For ticking chunks, it updates the persistent entity manager (soon to be completely nuked by EntitySliceManager, which
-+ will also need to be updated but with far less implications)
-+ It also shoves the scheduled block ticks into the tick scheduler
-+
-+ For entity ticking chunks, updates the entity manager (see above)
-+ */
-+
+ static final int NEIGHBOUR_RADIUS = 2;
+ private long fullNeighbourChunksLoadedBitset;
+
@@ -10816,45 +12064,47 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0;
+ }
+
-+ // returns true if this chunk changed full status
++ // returns true if this chunk changed pending full status
++ // must hold scheduling lock
+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) {
-+ final long before = this.fullNeighbourChunksLoadedBitset;
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset |= (1L << index);
-+ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset);
++ return this.updatePendingStatus();
+ }
+
-+ // returns true if this chunk changed full status
++ // returns true if this chunk changed pending full status
++ // must hold scheduling lock
+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) {
-+ final long before = this.fullNeighbourChunksLoadedBitset;
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index);
-+ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset);
++ return this.updatePendingStatus();
+ }
+
++ private static long getLoadedMask(final int radius) {
++ long mask = 0L;
++ for (int dx = -radius; dx <= radius; ++dx) {
++ for (int dz = -radius; dz <= radius; ++dz) {
++ mask |= (1L << getFullNeighbourIndex(dx, dz));
++ }
++ }
++
++ return mask;
++ }
++
++ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0);
++ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1);
++ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2);
++
+ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) {
-+ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)))
+ switch (radius) {
+ case 0: {
-+ return (bitset & (1L << getFullNeighbourIndex(0, 0))) != 0L;
++ return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0;
+ }
+ case 1: {
-+ long mask = 0L;
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ mask |= (1L << getFullNeighbourIndex(dx, dz));
-+ }
-+ }
-+ return (bitset & mask) == mask;
++ return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1;
+ }
+ case 2: {
-+ long mask = 0L;
-+ for (int dx = -2; dx <= 2; ++dx) {
-+ for (int dz = -2; dz <= 2; ++dz) {
-+ mask |= (1L << getFullNeighbourIndex(dx, dz));
-+ }
-+ }
-+ return (bitset & mask) == mask;
++ return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2;
+ }
+
+ default: {
@@ -10863,22 +12113,15 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
-+ // upper 16 bits are pending status, lower 16 bits are current status
-+ private volatile long chunkStatus;
-+ private static final long PENDING_STATUS_MASK = Long.MIN_VALUE >> 31;
-+ private static final FullChunkStatus[] CHUNK_STATUS_BY_ID = FullChunkStatus.values();
-+ private static final VarHandle CHUNK_STATUS_HANDLE = ConcurrentUtil.getVarHandle(NewChunkHolder.class, "chunkStatus", long.class);
-+
-+ public static FullChunkStatus getCurrentChunkStatus(final long encoded) {
-+ return CHUNK_STATUS_BY_ID[(int)encoded];
-+ }
-+
-+ public static FullChunkStatus getPendingChunkStatus(final long encoded) {
-+ return CHUNK_STATUS_BY_ID[(int)(encoded >>> 32)];
-+ }
++ // only updated while holding scheduling lock
++ private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE;
++ // updated while holding no locks, but adds a ticket before to prevent pending status from dropping
++ // so, current will never update to a value higher than pending
++ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE;
+
+ public FullChunkStatus getChunkStatus() {
-+ return getCurrentChunkStatus(((long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this)));
++ // no volatile access, access off-main is considered racey anyways
++ return this.currentFullChunkStatus;
+ }
+
+ public boolean isEntityTickingReady() {
@@ -10894,120 +12137,43 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ private static FullChunkStatus getStatusForBitset(final long bitset) {
-+ if (areNeighboursFullLoaded(bitset, 2)) {
++ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) {
+ return FullChunkStatus.ENTITY_TICKING;
-+ } else if (areNeighboursFullLoaded(bitset, 1)) {
++ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) {
+ return FullChunkStatus.BLOCK_TICKING;
-+ } else if (areNeighboursFullLoaded(bitset, 0)) {
++ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) {
+ return FullChunkStatus.FULL;
+ } else {
+ return FullChunkStatus.INACCESSIBLE;
+ }
+ }
+
-+ // note: only while updating ticket level, so holds ticket update lock + scheduling lock
-+ protected final boolean onTicketUpdate(final FullChunkStatus oldState, final FullChunkStatus newState) {
-+ if (oldState == newState) {
-+ return false;
-+ }
-+
-+ // preserve border request after full status complete, as it does not set anything in the bitset
-+ FullChunkStatus byNeighbours = getStatusForBitset(this.fullNeighbourChunksLoadedBitset);
-+ if (byNeighbours == FullChunkStatus.INACCESSIBLE && newState.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
-+ byNeighbours = FullChunkStatus.FULL;
-+ }
-+
-+ final FullChunkStatus toSet;
-+
-+ if (newState.isOrAfter(byNeighbours)) {
-+ // must clamp to neighbours level, even though we have the ticket level
-+ toSet = byNeighbours;
-+ } else {
-+ // must clamp to ticket level, even though we have the neighbours
-+ toSet = newState;
-+ }
-+
-+ long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);
-+
-+ if (curr == ((long)toSet.ordinal() | ((long)toSet.ordinal() << 32))) {
-+ // nothing to do
-+ return false;
-+ }
-+
-+ int failures = 0;
-+ for (;;) {
-+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toSet.ordinal() << 32);
-+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) {
-+ return true;
-+ }
++ // must hold scheduling lock
++ // returns whether the pending status was changed
++ private boolean updatePendingStatus() {
++ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock
+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
++ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset);
++ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
++ // the bitset is only for chunks that have gone through the status updater
++ // but here we are ready to go to FULL
++ pending = FullChunkStatus.FULL;
+ }
-+ }
+
-+ protected final boolean onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
-+ FullChunkStatus oldState = getStatusForBitset(bitsetBefore);
-+ FullChunkStatus newState = getStatusForBitset(bitsetAfter);
-+ final FullChunkStatus currStateTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel);
-+ if (oldState.isOrAfter(currStateTicketLevel)) {
-+ oldState = currStateTicketLevel;
-+ }
-+ if (newState.isOrAfter(currStateTicketLevel)) {
-+ newState = currStateTicketLevel;
-+ }
-+ // preserve border request after full status complete, as it does not set anything in the bitset
-+ if (newState == FullChunkStatus.INACCESSIBLE && currStateTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
-+ newState = FullChunkStatus.FULL;
++ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel
++ // cannot set above ticket level
++ pending = byTicketLevel;
+ }
+
-+ if (oldState == newState) {
++ if (this.pendingFullChunkStatus == pending) {
+ return false;
+ }
+
-+ int failures = 0;
-+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) {
-+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)newState.ordinal() << 32);
-+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) {
-+ return true;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ private boolean queueBorderFullStatus(final boolean loaded, final List<NewChunkHolder> changedFullStatus) {
-+ final FullChunkStatus toStatus = loaded ? FullChunkStatus.FULL : FullChunkStatus.INACCESSIBLE;
-+
-+ int failures = 0;
-+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) {
-+ final FullChunkStatus currPending = getPendingChunkStatus(curr);
-+ if (loaded && currPending != FullChunkStatus.INACCESSIBLE) {
-+ throw new IllegalStateException("Expected " + FullChunkStatus.INACCESSIBLE + " for pending, but got " + currPending);
-+ }
++ this.pendingFullChunkStatus = pending;
+
-+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toStatus.ordinal() << 32);
-+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) {
-+ if ((int)(update) != (int)(update >>> 32)) {
-+ changedFullStatus.add(this);
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
++ return true;
+ }
+
-+ // only call on main thread, must hold ticket level and scheduling lock
+ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS);
+ try {
@@ -11030,73 +12196,40 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
-+ private FullChunkStatus updateCurrentState(final FullChunkStatus to) {
-+ int failures = 0;
-+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) {
-+ final long update = (curr & PENDING_STATUS_MASK) | (long)to.ordinal();
-+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) {
-+ return getPendingChunkStatus(curr);
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) {
-+ this.world.getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus);
++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus);
+ }
+
+ private boolean processingFullStatus = false;
+
++ private void updateCurrentState(final FullChunkStatus to) {
++ this.currentFullChunkStatus = to;
++ }
++
+ // only to be called on the main thread, no locks need to be held
+ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
+
+ boolean ret = false;
+
+ if (this.processingFullStatus) {
-+ // we cannot process updates recursively
++ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status
+ return ret;
+ }
+
-+ // note: use opaque reads for chunk status read since we need it to be atomic
-+
-+ // test if anything changed
-+ long statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this);
-+ if ((int)statusCheck == (int)(statusCheck >>> 32)) {
-+ // nothing changed
-+ return ret;
-+ }
-+
-+ final ChunkTaskScheduler scheduler = this.scheduler;
-+ final ChunkHolderManager holderManager = scheduler.chunkHolderManager;
-+ final int ticketKeep;
-+ final Long ticketId = Long.valueOf(holderManager.getNextStatusUpgradeId());
-+ final ReentrantAreaLock.Node ticketLock = holderManager.ticketLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ ticketKeep = this.currentTicketLevel;
-+ statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this);
-+ // handle race condition where ticket level and target status is updated concurrently
-+ if ((int)statusCheck == (int)(statusCheck >>> 32)) {
-+ // nothing changed
-+ return ret;
-+ }
-+ holderManager.addTicketAtLevel(TicketType.STATUS_UPGRADE, CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ), ticketKeep, ticketId, false);
-+ } finally {
-+ holderManager.ticketLockArea.unlock(ticketLock);
-+ }
-+
+ this.processingFullStatus = true;
+ try {
+ for (;;) {
-+ final long currStateEncoded = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this);
-+ final FullChunkStatus currState = getCurrentChunkStatus(currStateEncoded);
-+ FullChunkStatus nextState = getPendingChunkStatus(currStateEncoded);
-+ if (currState == nextState) {
-+ if (nextState == FullChunkStatus.INACCESSIBLE) {
++ // check if we have any remaining work to do
++
++ // we do not need to hold the scheduling lock to read pending, as changes to pending
++ // will queue a status update
++
++ final FullChunkStatus pending = this.pendingFullChunkStatus;
++ FullChunkStatus current = this.currentFullChunkStatus;
++
++ if (pending == current) {
++ if (pending == FullChunkStatus.INACCESSIBLE) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
@@ -11104,10 +12237,17 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
-+ break;
++ return ret;
+ }
+
++ ret = true;
++
++ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we
++ // do not need to consider cases where the ticket level may decrease during this call by asynchronous
++ // ticket changes
++
+ // chunks cannot downgrade state while status is pending a change
++ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE
+ final LevelChunk chunk = (LevelChunk)this.currentChunk;
+
+ // Note: we assume that only load/unload contain plugin logic
@@ -11115,60 +12255,54 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // being changed (i.e during load it is possible it will try to set to full ticking)
+ // in order to allow this change, we also need this plugin logic to be contained strictly after all
+ // of the chunk system load callbacks are invoked
-+ if (nextState.isOrAfter(currState)) {
++ if (pending.isOrAfter(current)) {
+ // state upgrade
-+ if (!currState.isOrAfter(FullChunkStatus.FULL) && nextState.isOrAfter(FullChunkStatus.FULL)) {
-+ nextState = this.updateCurrentState(FullChunkStatus.FULL);
-+ holderManager.ensureInAutosave(this);
-+ chunk.pushChunkIntoLoadedMap();
++ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) {
++ this.updateCurrentState(FullChunkStatus.FULL);
++ this.scheduler.chunkHolderManager.ensureInAutosave(this);
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+ chunk.onChunkLoad(this);
++ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder);
+ this.onFullChunkLoadChange(true, changedFullStatus);
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk);
+ }
+
-+ if (!currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
-+ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
++ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
++ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+ chunk.onChunkTicking(this);
++ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk);
+ }
+
-+ if (!currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
-+ nextState = this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
++ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
++ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING);
-+ chunk.onChunkEntityTicking(this);
++ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk);
+ }
+ } else {
-+ if (currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
++ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+ chunk.onChunkNotEntityTicking(this);
-+ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
++ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
++ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ }
+
-+ if (currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
++ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+ chunk.onChunkNotTicking(this);
-+ nextState = this.updateCurrentState(FullChunkStatus.FULL);
++ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder);
++ this.updateCurrentState(FullChunkStatus.FULL);
+ }
+
-+ if (currState.isOrAfter(FullChunkStatus.FULL) && !nextState.isOrAfter(FullChunkStatus.FULL)) {
++ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) {
+ this.onFullChunkLoadChange(false, changedFullStatus);
+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE);
-+ chunk.onChunkUnload(this);
-+ nextState = this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
++ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder);
++ this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
+ }
+ }
-+
-+ ret = true;
+ }
+ } finally {
+ this.processingFullStatus = false;
-+ holderManager.removeTicketAtLevel(TicketType.STATUS_UPGRADE, this.chunkX, this.chunkZ, ticketKeep, ticketId);
+ }
-+
-+ return ret;
+ }
+
+ // note: must hold scheduling lock
@@ -11214,7 +12348,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // need to tell future statuses to complete if cancelled
+ do {
+ this.completeStatusConsumers0(status, chunk);
-+ } while (chunk == null && status != (status = status.getNextStatus()));
++ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus()));
+ }
+
+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) {
@@ -11230,8 +12364,6 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ for (final Consumer<ChunkAccess> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
@@ -11248,19 +12380,6 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+
+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) {
-+ // need to tell future statuses to complete if cancelled
-+ final FullChunkStatus max = CHUNK_STATUS_BY_ID[CHUNK_STATUS_BY_ID.length - 1];
-+
-+ for (;;) {
-+ this.completeFullStatusConsumers0(status, chunk);
-+ if (chunk != null || status == max) {
-+ break;
-+ }
-+ status = CHUNK_STATUS_BY_ID[status.ordinal() + 1];
-+ }
-+ }
-+
-+ private void completeFullStatusConsumers0(final FullChunkStatus status, final LevelChunk chunk) {
+ final List<Consumer<LevelChunk>> consumers;
+ consumers = this.fullStatusWaiters.remove(status);
+
@@ -11273,8 +12392,6 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ for (final Consumer<LevelChunk> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
-+ } catch (final ThreadDeath thr) {
-+ throw thr;
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
@@ -11347,7 +12464,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.neighboursWaitingForUs.clear();
+ }
+ // reset priority, we have nothing left to generate to
-+ this.setPriority(PrioritisedExecutor.Priority.NORMAL);
++ this.setPriority(null);
+ this.checkUnload();
+ return;
+ }
@@ -11405,10 +12522,9 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ if (newStatus == ChunkStatus.FULL) {
+ this.lockPriority();
-+ // must use oldTicketLevel, we hold the schedule lock but not the ticket level lock
-+ // however, schedule lock needs to be held for ticket level callback, so we're fine here
-+ if (ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) {
-+ this.queueBorderFullStatus(true, changedLoadStatus);
++ // try to push pending to FULL
++ if (this.updatePendingStatus()) {
++ changedLoadStatus.add(this);
+ }
+ }
+
@@ -11429,7 +12545,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ this.requestedGenStatus = null;
+ }
+ // reached final stage, so stop scheduling now
-+ this.setPriority(PrioritisedExecutor.Priority.NORMAL);
++ this.setPriority(null);
+ this.checkUnload();
+
+ this.scheduleNeighbours(needsScheduling, scheduleList);
@@ -11473,7 +12589,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ if (thr != null) {
+ if (this.genTaskException != null) {
-+ // first one is probably the TRUE problem
++ LOGGER.warn("Ignoring exception for " + this.toString(), thr);
+ return;
+ }
+ // don't set generation task to null, so that scheduling will not attempt to create another task and it
@@ -11534,8 +12650,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
+
-+ public SaveStat save(final boolean shutdown, final boolean unloading) {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
++ public SaveStat save(final boolean shutdown) {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
+
+ ChunkAccess chunk = this.getCurrentChunk();
+ PoiChunk poi = this.getPoiChunk();
@@ -11560,24 +12676,24 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ }
+ }
+
-+ boolean canSaveChunk = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) &&
-+ (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved()));
-+ boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty());
++ final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk);
++
++ // can only synchronously save worldgen chunks during shutdown
++ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved()));
++ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty());
+ boolean canSaveEntities = entities != null;
+
-+ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper
-+ if (canSaveChunk) {
-+ canSaveChunk = this.saveChunk(chunk, unloading);
-+ }
-+ if (canSavePOI) {
-+ canSavePOI = this.savePOI(poi, unloading);
-+ }
-+ if (canSaveEntities) {
-+ // on shutdown, we need to force transient entity chunks to save
-+ canSaveEntities = this.saveEntities(entities, unloading || shutdown);
-+ if (unloading || shutdown) {
-+ this.lastEntityUnload = null;
-+ }
++ if (canSaveChunk) {
++ canSaveChunk = this.saveChunk(chunk, false);
++ }
++ if (canSavePOI) {
++ canSavePOI = this.savePOI(poi, false);
++ }
++ if (canSaveEntities) {
++ // on shutdown, we need to force transient entity chunks to save
++ canSaveEntities = this.saveEntities(entities, shutdown);
++ if (shutdown) {
++ this.lastEntityUnload = null;
+ }
+ }
+
@@ -11588,10 +12704,10 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+
+ private final ServerLevel world;
+ private final ChunkAccess chunk;
-+ private final ChunkSerializer.AsyncSaveData asyncSaveData;
++ private final AsyncChunkSaveData asyncSaveData;
+ private final NewChunkHolder toComplete;
+
-+ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final ChunkSerializer.AsyncSaveData asyncSaveData,
++ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData,
+ final NewChunkHolder toComplete) {
+ this.world = world;
+ this.chunk = chunk;
@@ -11603,36 +12719,33 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ public void run() {
+ final CompoundTag toSerialize;
+ try {
-+ toSerialize = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData);
-+ } catch (final ThreadDeath death) {
-+ throw death;
++ toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData);
+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + this.world.getWorld().getName() + "', falling back to synchronous save", throwable);
-+ this.world.chunkTaskScheduler.scheduleChunkTask(this.chunk.locX, this.chunk.locZ, () -> {
++ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable);
++ final ChunkPos pos = this.chunk.getPos();
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
+ final CompoundTag synchronousSave;
+ try {
-+ synchronousSave = ChunkSerializer.saveChunk(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData);
-+ } catch (final ThreadDeath death) {
-+ throw death;
++ synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData);
+ } catch (final Throwable throwable2) {
-+ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "', chunk data will be lost", throwable2);
-+ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(null);
++ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2);
++ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ return;
+ }
+
-+ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(synchronousSave);
-+ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "' synchronously");
++ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave);
++ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously");
+
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ return;
+ }
-+ this.toComplete.completeAsyncChunkDataSave(toSerialize);
++ this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize);
+ }
+
+ @Override
+ public String toString() {
+ return "AsyncChunkSerializeTask{" +
-+ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + this.world.getWorld().getName() + "\"}" +
++ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" +
+ "}";
+ }
+ }
@@ -11640,49 +12753,49 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) {
+ if (!chunk.isUnsaved()) {
+ if (unloading) {
-+ this.completeAsyncChunkDataSave(null);
++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+ return false;
+ }
+ boolean completing = false;
++ boolean failedAsyncPrepare = false;
+ try {
-+ if (unloading) {
++ if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) {
+ try {
-+ final ChunkSerializer.AsyncSaveData asyncSaveData = ChunkSerializer.getAsyncSaveData(this.world, chunk);
++ final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk);
+
+ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this));
+
+ this.chunkDataUnload.task().setTask(task);
+
-+ task.queue();
-+
+ chunk.setUnsaved(false);
+
++ task.queue();
++
+ return true;
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', falling back to synchronous save", thr);
++ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr);
++ failedAsyncPrepare = true;
+ // fall through to synchronous save
+ }
+ }
+
-+ final CompoundTag save = ChunkSerializer.saveChunk(this.world, chunk, null);
++ final CompoundTag save = ChunkSerializer.write(this.world, chunk);
+
+ if (unloading) {
+ completing = true;
-+ this.completeAsyncChunkDataSave(save);
-+ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "' synchronously");
++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save);
++ if (failedAsyncPrepare) {
++ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously");
++ }
+ } else {
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ }
+ chunk.setUnsaved(false);
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (unloading && !completing) {
-+ this.completeAsyncChunkDataSave(null);
++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+ }
+
@@ -11703,7 +12816,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ try {
+ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING);
+ } catch (final Exception ex) {
-+ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', data on disk will be replaced", ex);
++ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex);
+ }
+ }
+
@@ -11713,7 +12826,7 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ // don't override the data on disk with nothing
+ return false;
+ } else {
-+ EntityStorage.copyEntities(mergeFrom, save);
++ ChunkEntitySlices.copyEntities(mergeFrom, save);
+ }
+ }
+ if (save == null && this.lastEntitySaveNull) {
@@ -11725,10 +12838,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ if (unloading) {
+ this.lastEntityUnload = save;
+ }
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
++ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ return true;
@@ -11751,10 +12862,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ if (unloading) {
+ this.poiDataUnload.completable().complete(save);
+ }
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
++ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ return true;
@@ -11764,13 +12873,10 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ public String toString() {
+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
+ final ChunkEntitySlices entityChunk = this.entityChunk;
-+ final long chunkStatus = this.chunkStatus;
-+ final int fullChunkStatus = (int)chunkStatus;
-+ final int pendingChunkStatus = (int)(chunkStatus >>> 32);
-+ final FullChunkStatus currentFullStatus = fullChunkStatus < 0 || fullChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[fullChunkStatus];
-+ final FullChunkStatus pendingFullStatus = pendingChunkStatus < 0 || pendingChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[pendingChunkStatus];
++ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus;
++ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus;
+ return "NewChunkHolder{" +
-+ "world=" + this.world.getWorld().getName() +
++ "world=" + WorldUtil.getWorldName(this.world) +
+ ", chunkX=" + this.chunkX +
+ ", chunkZ=" + this.chunkZ +
+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) +
@@ -11782,36 +12888,53 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ ", priority=" + this.priority +
+ ", priorityLocked=" + this.priorityLocked +
+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority +
-+ ", effective_priority=" + this.getEffectivePriority() +
++ ", effective_priority=" + this.getEffectivePriority(null) +
+ ", oldTicketLevel=" + this.oldTicketLevel +
+ ", currentTicketLevel=" + this.currentTicketLevel +
+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk +
+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset +
-+ ", chunkStatusRaw=" + chunkStatus +
+ ", currentChunkStatus=" + currentFullStatus +
+ ", pendingChunkStatus=" + pendingFullStatus +
+ ", is_unload_safe=" + this.isSafeToUnload() +
-+ ", killed=" + this.killed +
++ ", killed=" + this.unloaded +
+ '}';
+ }
+
-+ private static JsonElement serializeCompletable(final Completable<?> completable) {
++ private static JsonElement serializeStacktraceElement(final StackTraceElement element) {
++ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
++ }
++
++ private static JsonObject serializeCompletable(final Completable<?> completable) {
++ final JsonObject ret = new JsonObject();
++
+ if (completable == null) {
-+ return new JsonPrimitive("null");
++ return ret;
+ }
+
-+ final JsonObject ret = new JsonObject();
++ ret.addProperty("valid", Boolean.TRUE);
++
+ final boolean isCompleted = completable.isCompleted();
+ ret.addProperty("completed", Boolean.valueOf(isCompleted));
+
+ if (isCompleted) {
-+ ret.addProperty("completed_exceptionally", Boolean.valueOf(completable.getThrowable() != null));
++ final Throwable throwable = completable.getThrowable();
++ if (throwable != null) {
++ final JsonArray throwableJson = new JsonArray();
++ ret.add("throwable", throwableJson);
++
++ for (final StackTraceElement element : throwable.getStackTrace()) {
++ throwableJson.add(serializeStacktraceElement(element));
++ }
++ } else {
++ final Object result = completable.getResult();
++ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName()));
++ }
+ }
+
+ return ret;
+ }
+
-+ // holds ticket and scheduling lock
++ // (probably) holds ticket and scheduling lock
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
@@ -11861,8 +12984,8 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ neighbour.addProperty("waiting_for", Objects.toString(status));
+ }
+
-+ ret.addProperty("fullchunkstatus", Objects.toString(this.getChunkStatus()));
-+ ret.addProperty("fullchunkstatus_raw", Long.valueOf(this.chunkStatus));
++ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus));
++ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus));
+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus));
+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null));
@@ -11885,18 +13008,18 @@ index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb
+ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal()));
+ }
+
-+ ret.addProperty("killed", Boolean.valueOf(this.killed));
++ ret.addProperty("killed", Boolean.valueOf(this.unloaded));
+
+ return ret;
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
new file mode 100644
-index 0000000000000000000000000000000000000000..b4c56bf12dc8dd17452210ece4fd67411cc6b2fd
+index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c7aa6a3a8
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
@@ -0,0 +1,215 @@
-+package io.papermc.paper.chunk.system.scheduling;
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
@@ -11939,11 +13062,11 @@ index 0000000000000000000000000000000000000000..b4c56bf12dc8dd17452210ece4fd6741
+ }
+
+ // returns false if cancelled
-+ protected boolean markExecuting() {
++ public boolean markExecuting() {
+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
+ }
+
-+ protected boolean isMarkedExecuted() {
++ public boolean isMarkedExecuted() {
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
@@ -12111,28 +13234,30 @@ index 0000000000000000000000000000000000000000..b4c56bf12dc8dd17452210ece4fd6741
+
+ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority);
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
new file mode 100644
-index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276f626405d
+index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java
-@@ -0,0 +1,1477 @@
-+package io.papermc.paper.chunk.system.scheduling;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
+@@ -0,0 +1,1457 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import it.unimi.dsi.fastutil.HashCommon;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteMap;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
++import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
-+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.LockSupport;
+
+public abstract class ThreadedTicketLevelPropagator {
@@ -12143,15 +13268,20 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ private static final int LEVEL_BITS = SECTION_SHIFT;
+ private static final int LEVEL_COUNT = 1 << LEVEL_BITS;
+ private static final int MIN_SOURCE_LEVEL = 1;
-+ // we limit the max source to 62 because the depropagation code _must_ attempt to depropagate
-+ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in depropagation
++ // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate
++ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation
+ private static final int MAX_SOURCE_LEVEL = 62;
+
++ private static int getMaxSchedulingRadius() {
++ return 2 * ChunkTaskScheduler.getMaxAccessRadius();
++ }
++
+ private final UpdateQueue updateQueue;
-+ private final ConcurrentHashMap<Coordinate, Section> sections = new ConcurrentHashMap<>();
++ private final ConcurrentLong2ReferenceChainedHashTable<Section> sections;
+
+ public ThreadedTicketLevelPropagator() {
+ this.updateQueue = new UpdateQueue();
++ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>();
+ }
+
+ // must hold ticket lock for:
@@ -12164,7 +13294,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
-+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ);
++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ Section section = this.sections.get(coordinate);
+ if (section == null) {
+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) {
@@ -12196,7 +13326,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
-+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ);
++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null) {
@@ -12244,7 +13374,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ return false;
+ }
+
-+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null || section.queuedSources.isEmpty()) {
@@ -12280,7 +13410,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ final boolean ret;
+ try {
+ // first, check if this update was stolen
-+ if (section != this.sections.get(new Coordinate(sectionX, sectionZ))) {
++ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) {
+ // occurs when a stolen update deletes this section
+ // it is possible that another update is scheduled, but that one will have the correct section
+ if (node != null) {
@@ -12316,7 +13446,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ // set current level and current source to new source
+ section.levels[pos] = (short)(newSource | (newSource << 8));
+ // must add to updated positions in case this is final
-+ propagator.updatedPositions.put(Coordinate.key(posX, posZ), (byte)newSource);
++ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource);
+ if (newSource != 0) {
+ // queue increase with new source level
+ propagator.appendToIncreaseQueue(
@@ -12357,9 +13487,9 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
-+ final Coordinate coordinate = new Coordinate(offX, offZ);
-+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final Coordinate keyInMap) -> {
-+ return new Section(Coordinate.x(keyInMap.key), Coordinate.z(keyInMap.key));
++ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
++ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> {
++ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap));
+ });
+
+ // increase ref count
@@ -12385,12 +13515,12 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ for (int dx = -1; dx <= 1; ++dx) {
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
-+ final Coordinate coordinate = new Coordinate(offX, offZ);
++ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
+ final Section neighbour = this.sections.get(coordinate);
+
+ if (neighbour == null) {
+ if (oldSourceSize == 0 && (dx | dz) != 0) {
-+ // since we don't have sources, this section is allowed to null
++ // since we don't have sources, this section is allowed to be null
+ continue;
+ }
+ throw new IllegalStateException("??");
@@ -12422,7 +13552,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+
+ if (!propagator.updatedPositions.isEmpty()) {
+ // now we can actually update the ticket levels in the chunk holders
-+ final int maxScheduleRadius = 2 * ChunkTaskScheduler.getMaxAccessRadius();
++ final int maxScheduleRadius = getMaxSchedulingRadius();
+
+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time
+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock(
@@ -12464,10 +13594,8 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ Propagator propagator = null;
+
+ for (;;) {
-+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextToUpdate(maxOrder);
++ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder);
+ if (toUpdate == null) {
-+ this.updateQueue.awaitFirst(maxOrder);
-+
+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) {
+ if (propagator != null) {
+ Propagator.returnPropagator(propagator);
@@ -12486,66 +13614,56 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
++ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer
++ // for the last update node being handled
+ private static final class UpdateQueue {
+
+ private volatile UpdateQueueNode head;
+ private volatile UpdateQueueNode tail;
-+ private volatile UpdateQueueNode lastUpdating;
+
-+ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class);
-+ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class);
-+ protected static final VarHandle LAST_UPDATING = ConcurrentUtil.getVarHandle(UpdateQueue.class, "lastUpdating", UpdateQueueNode.class);
++ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class);
++ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class);
+
+ /* head */
+
-+ protected final void setHeadPlain(final UpdateQueueNode newHead) {
++ private final void setHeadPlain(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.set(this, newHead);
+ }
+
-+ protected final void setHeadOpaque(final UpdateQueueNode newHead) {
++ private final void setHeadOpaque(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.setOpaque(this, newHead);
+ }
+
-+ protected final UpdateQueueNode getHeadPlain() {
++ private final UpdateQueueNode getHeadPlain() {
+ return (UpdateQueueNode)HEAD_HANDLE.get(this);
+ }
+
-+ protected final UpdateQueueNode getHeadOpaque() {
++ private final UpdateQueueNode getHeadOpaque() {
+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this);
+ }
+
-+ protected final UpdateQueueNode getHeadAcquire() {
++ private final UpdateQueueNode getHeadAcquire() {
+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this);
+ }
+
+ /* tail */
+
-+ protected final void setTailPlain(final UpdateQueueNode newTail) {
++ private final void setTailPlain(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.set(this, newTail);
+ }
+
-+ protected final void setTailOpaque(final UpdateQueueNode newTail) {
++ private final void setTailOpaque(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.setOpaque(this, newTail);
+ }
+
-+ protected final UpdateQueueNode getTailPlain() {
++ private final UpdateQueueNode getTailPlain() {
+ return (UpdateQueueNode)TAIL_HANDLE.get(this);
+ }
+
-+ protected final UpdateQueueNode getTailOpaque() {
++ private final UpdateQueueNode getTailOpaque() {
+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this);
+ }
+
-+ /* lastUpdating */
-+
-+ protected final UpdateQueueNode getLastUpdatingVolatile() {
-+ return (UpdateQueueNode)LAST_UPDATING.getVolatile(this);
-+ }
-+
-+ protected final UpdateQueueNode compareAndExchangeLastUpdatingVolatile(final UpdateQueueNode expect, final UpdateQueueNode update) {
-+ return (UpdateQueueNode)LAST_UPDATING.compareAndExchange(this, expect, update);
-+ }
-+
+ public UpdateQueue() {
+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null);
+ dummy.order = -1L;
@@ -12578,44 +13696,55 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ public UpdateQueueNode acquireNextToUpdate(final long maxOrder) {
-+ int failures = 0;
-+ for (UpdateQueueNode prev = this.getLastUpdatingVolatile();;) {
-+ UpdateQueueNode next = prev == null ? this.peek() : prev.next;
++ private static void await(final UpdateQueueNode node) {
++ final Thread currThread = Thread.currentThread();
++ // we do not use add-blocking because we use the nullability of the section to block
++ // remove() does not begin to poll from the wait queue until the section is null'd,
++ // and so provided we check the nullability before parking there is no ordering of these operations
++ // such that remove() finishes polling from the wait queue while section is not null
++ node.add(currThread);
+
-+ if (next == null || next.order > maxOrder) {
-+ return null;
++ // wait until completed
++ while (node.getSectionVolatile() != null) {
++ LockSupport.park();
++ }
++ }
++
++ public UpdateQueueNode acquireNextOrWait(final long maxOrder) {
++ final List<UpdateQueueNode> blocking = new ArrayList<>();
++
++ node_search:
++ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) {
++ if (curr.getSectionVolatile() == null) {
++ continue;
+ }
+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
++ if (curr.getUpdatingVolatile()) {
++ blocking.add(curr);
++ continue;
+ }
+
-+ if (prev == (prev = this.compareAndExchangeLastUpdatingVolatile(prev, next))) {
-+ return next;
++ for (int i = 0, len = blocking.size(); i < len; ++i) {
++ final UpdateQueueNode node = blocking.get(i);
++
++ if (node.intersects(curr)) {
++ continue node_search;
++ }
+ }
+
-+ ++failures;
-+ }
-+ }
++ if (curr.getAndSetUpdatingVolatile(true)) {
++ blocking.add(curr);
++ continue;
++ }
+
-+ public void awaitFirst(final long maxOrder) {
-+ final UpdateQueueNode earliest = this.peek();
-+ if (earliest == null || earliest.order > maxOrder) {
-+ return;
++ return curr;
+ }
+
-+ final Thread currThread = Thread.currentThread();
-+ // we do not use add-blocking because we use the nullability of the section to block
-+ // remove() does not begin to poll from the wait queue until the section is null'd,
-+ // and so provided we check the nullability before parking there is no ordering of these operations
-+ // such that remove() finishes polling from the wait queue while section is not null
-+ earliest.add(currThread);
-+
-+ // wait until completed
-+ while (earliest.getSectionVolatile() != null) {
-+ LockSupport.park();
++ if (!blocking.isEmpty()) {
++ await(blocking.get(0));
+ }
++
++ return null;
+ }
+
+ public UpdateQueueNode peek() {
@@ -12702,77 +13831,106 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ // each node also represents a set of waiters, represented by the MTQ
+ // if the queue is add-blocked, then the update is complete
+ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> {
++ private final int sectionX;
++ private final int sectionZ;
++
+ private long order;
-+ private Section section;
++ private volatile Section section;
+ private volatile UpdateQueueNode next;
++ private volatile boolean updating;
+
-+ protected static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class);
-+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class);
++ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class);
++ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class);
++ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class);
+
+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) {
++ if (section == null) {
++ this.sectionX = this.sectionZ = 0;
++ } else {
++ this.sectionX = section.sectionX;
++ this.sectionZ = section.sectionZ;
++ }
++
+ SECTION_HANDLE.set(this, section);
+ NEXT_HANDLE.set(this, next);
+ }
+
++ public boolean intersects(final UpdateQueueNode other) {
++ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ));
++
++ // intersection radius is ticket update radius (1) + scheduling radius
++ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT));
++ }
++
+ /* section */
+
-+ protected final Section getSectionPlain() {
++ private final Section getSectionPlain() {
+ return (Section)SECTION_HANDLE.get(this);
+ }
+
-+ protected final Section getSectionVolatile() {
++ private final Section getSectionVolatile() {
+ return (Section)SECTION_HANDLE.getVolatile(this);
+ }
+
-+ protected final void setSectionPlain(final Section update) {
++ private final void setSectionPlain(final Section update) {
+ SECTION_HANDLE.set(this, update);
+ }
+
-+ protected final void setSectionOpaque(final Section update) {
++ private final void setSectionOpaque(final Section update) {
+ SECTION_HANDLE.setOpaque(this, update);
+ }
+
-+ protected final void setSectionVolatile(final Section update) {
++ private final void setSectionVolatile(final Section update) {
+ SECTION_HANDLE.setVolatile(this, update);
+ }
+
-+ protected final Section getAndSetSectionVolatile(final Section update) {
++ private final Section getAndSetSectionVolatile(final Section update) {
+ return (Section)SECTION_HANDLE.getAndSet(this, update);
+ }
+
-+ protected final Section compareExchangeSectionVolatile(final Section expect, final Section update) {
++ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) {
+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ /* next */
+
-+ protected final UpdateQueueNode getNextPlain() {
++ private final UpdateQueueNode getNextPlain() {
+ return (UpdateQueueNode)NEXT_HANDLE.get(this);
+ }
+
-+ protected final UpdateQueueNode getNextOpaque() {
++ private final UpdateQueueNode getNextOpaque() {
+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this);
+ }
+
-+ protected final UpdateQueueNode getNextAcquire() {
++ private final UpdateQueueNode getNextAcquire() {
+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this);
+ }
+
-+ protected final UpdateQueueNode getNextVolatile() {
++ private final UpdateQueueNode getNextVolatile() {
+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this);
+ }
+
-+ protected final void setNextPlain(final UpdateQueueNode next) {
++ private final void setNextPlain(final UpdateQueueNode next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
-+ protected final void setNextVolatile(final UpdateQueueNode next) {
++ private final void setNextVolatile(final UpdateQueueNode next) {
+ NEXT_HANDLE.setVolatile(this, next);
+ }
+
-+ protected final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) {
++ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) {
+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set);
+ }
++
++ /* updating */
++
++ private final boolean getUpdatingVolatile() {
++ return (boolean)UPDATING_HANDLE.getVolatile(this);
++ }
++
++ private final boolean getAndSetUpdatingVolatile(final boolean value) {
++ return (boolean)UPDATING_HANDLE.getAndSet(this, value);
++ }
+ }
+ }
+
@@ -12885,7 +14043,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0;
+ }
+
-+ protected final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) {
++ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) {
+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1);
+ // must have that encoded >= 0
+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE]
@@ -12909,14 +14067,14 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+
+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad
+ // must call setupEncodeOffset
-+ protected final void setupCaches(final ThreadedTicketLevelPropagator propagator,
++ private final void setupCaches(final ThreadedTicketLevelPropagator propagator,
+ final int centerSectionX, final int centerSectionZ,
+ final int rad) {
+ for (int dz = -rad; dz <= rad; ++dz) {
+ for (int dx = -rad; dx <= rad; ++dx) {
+ final int sectionX = centerSectionX + dx;
+ final int sectionZ = centerSectionZ + dz;
-+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ);
++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = propagator.sections.get(coordinate);
+
+ if (section == null) {
@@ -12928,15 +14086,15 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ protected final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) {
++ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) {
+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section;
+ }
+
-+ protected final Section getSection(final int sectionX, final int sectionZ) {
++ private final Section getSection(final int sectionX, final int sectionZ) {
+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset];
+ }
+
-+ protected final int getLevel(final int posX, final int posZ) {
++ private final int getLevel(final int posX, final int posZ) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF;
@@ -12945,17 +14103,17 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ return 0;
+ }
+
-+ protected final void setLevel(final int posX, final int posZ, final int to) {
++ private final void setLevel(final int posX, final int posZ, final int to) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short level = section.levels[index];
+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF));
-+ this.updatedPositions.put(Coordinate.key(posX, posZ), (byte)to);
++ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to);
+ }
+ }
+
-+ protected final void destroyCaches() {
++ private final void destroyCaches() {
+ Arrays.fill(this.sections, null);
+ }
+
@@ -12963,7 +14121,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS))
+ // next LEVEL_BITS (6) bits: propagated level [0, 63]
+ // propagation directions bitset (16 bits):
-+ protected static final long ALL_DIRECTIONS_BITSET = (
++ private static final long ALL_DIRECTIONS_BITSET = (
+ // z = -1
+ (1L << ((1 - 1) | ((1 - 1) << 2))) |
+ (1L << ((1 + 0) | ((1 - 1) << 2))) |
@@ -13027,27 +14185,27 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+
+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading
+ // updates for sources
-+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1;
++ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1;
+ // whether the propagation needs to check if its current level is equal to the expected level
+ // used only in increase propagation
-+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0;
++ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0;
+
-+ protected long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
-+ protected int increaseQueueInitialLength;
-+ protected long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
-+ protected int decreaseQueueInitialLength;
++ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
++ private int increaseQueueInitialLength;
++ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
++ private int decreaseQueueInitialLength;
+
-+ protected final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap();
++ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap();
+
-+ protected final long[] resizeIncreaseQueue() {
++ private final long[] resizeIncreaseQueue() {
+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
+ }
+
-+ protected final long[] resizeDecreaseQueue() {
++ private final long[] resizeDecreaseQueue() {
+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
+ }
+
-+ protected final void appendToIncreaseQueue(final long value) {
++ private final void appendToIncreaseQueue(final long value) {
+ final int idx = this.increaseQueueInitialLength++;
+ long[] queue = this.increaseQueue;
+ if (idx >= queue.length) {
@@ -13060,7 +14218,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ protected final void appendToDecreaseQueue(final long value) {
++ private final void appendToDecreaseQueue(final long value) {
+ final int idx = this.decreaseQueueInitialLength++;
+ long[] queue = this.decreaseQueue;
+ if (idx >= queue.length) {
@@ -13073,7 +14231,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ protected final void performIncrease() {
++ private final void performIncrease() {
+ long[] queue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.increaseQueueInitialLength;
@@ -13189,7 +14347,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF));
-+ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)toPropagate);
++ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate);
+
+ // queue next
+ if (toPropagate > 1) {
@@ -13217,7 +14375,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ protected final void performDecrease() {
++ private final void performDecrease() {
+ long[] queue = this.decreaseQueue;
+ long[] increaseQueue = this.increaseQueue;
+ int queueReadIndex = 0;
@@ -13340,7 +14498,7 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF));
-+ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)0);
++ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0);
+
+ if (sourceLevel != 0) {
+ // re-propagate source
@@ -13383,61 +14541,6 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ }
+
-+ private static final class Coordinate implements Comparable<Coordinate> {
-+
-+ public final long key;
-+
-+ public Coordinate(final long key) {
-+ this.key = key;
-+ }
-+
-+ public Coordinate(final int x, final int z) {
-+ this.key = key(x, z);
-+ }
-+
-+ public static long key(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
-+
-+ public static int x(final long key) {
-+ return (int)key;
-+ }
-+
-+ public static int z(final long key) {
-+ return (int)(key >>> 32);
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ return (int)HashCommon.mix(this.key);
-+ }
-+
-+ @Override
-+ public boolean equals(final Object obj) {
-+ if (this == obj) {
-+ return true;
-+ }
-+
-+ if (!(obj instanceof Coordinate other)) {
-+ return false;
-+ }
-+
-+ return this.key == other.key;
-+ }
-+
-+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain
-+ // is too large. So we should implement compareTo to help.
-+ @Override
-+ public int compareTo(final Coordinate other) {
-+ return Long.compare(this.key, other.key);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "[" + x(this.key) + "," + z(this.key) + "]";
-+ }
-+ }
-+
+ /*
+ private static final java.util.Random random = new java.util.Random(4L);
+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers =
@@ -13594,18 +14697,19 @@ index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276
+ }
+ */
+}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
new file mode 100644
-index 0000000000000000000000000000000000000000..f7b0e2564ac4bd2db1d2b2bdc230c9f52f8a21b7
+index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373ba811ba8b
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java
-@@ -0,0 +1,667 @@
-+package io.papermc.paper.chunk.system.scheduling.queue;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
+@@ -0,0 +1,668 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import io.papermc.paper.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
@@ -14267,1373 +15371,6989 @@ index 0000000000000000000000000000000000000000..f7b0e2564ac4bd2db1d2b2bdc230c9f5
+ }
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
-index a3f43dccb796f30f6e9389e1ae182f06e9024e96..92bf78112c1cc75173e4e735bdeec9695fe10df6 100644
---- a/src/main/java/io/papermc/paper/command/PaperCommand.java
-+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
-@@ -43,6 +43,7 @@ public final class PaperCommand extends Command {
- commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
- commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
- commands.put(Set.of("fixlight"), new FixLightCommand());
-+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
-
- return commands.entrySet().stream()
- .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..61fa1ab0f6550fec2b9d035ea45c72627eb990d4
+index 0000000000000000000000000000000000000000..1cf055447bfe235b806bfef6c95aa025f28cb239
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-@@ -0,0 +1,265 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import io.papermc.paper.command.CommandUtil;
-+import io.papermc.paper.command.PaperSubcommand;
-+import java.io.File;
-+import java.time.LocalDateTime;
-+import java.time.format.DateTimeFormatter;
-+import java.util.ArrayList;
-+import java.util.Collections;
-+import java.util.List;
-+import java.util.Locale;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.FullChunkStatus;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
+@@ -0,0 +1,138 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
++import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.ProtoChunk;
-+import org.bukkit.Bukkit;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class ChunkDebugCommand implements PaperSubcommand {
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++
++public final class ChunkFullTask extends ChunkProgressionTask implements Runnable {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class);
++
++ private final NewChunkHolder chunkHolder;
++ private final ChunkAccess fromChunk;
++ private final PrioritisedExecutor.PrioritisedTask convertToFullTask;
++
++ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ);
++ this.chunkHolder = chunkHolder;
++ this.fromChunk = fromChunk;
++ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority);
++ }
++
+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> this.doDebug(sender, args);
-+ case "chunkinfo" -> this.doChunkInfo(sender, args);
-+ case "holderinfo" -> this.doHolderInfo(sender, args);
-+ }
-+ return true;
++ public ChunkStatus getTargetStatus() {
++ return ChunkStatus.FULL;
+ }
+
+ @Override
-+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> {
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
-+ }
++ public void run() {
++ // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing
++ final LevelChunk chunk;
++ try {
++ // moved from the load from nbt stage into here
++ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk();
++ if (poiChunk == null) {
++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
++ } else {
++ poiChunk.load();
++ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk);
+ }
-+ case "holderinfo" -> {
-+ List<String> worldNames = new ArrayList<>();
-+ worldNames.add("*");
-+ for (org.bukkit.World world : Bukkit.getWorlds()) {
-+ worldNames.add(world.getName());
-+ }
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
-+ }
++
++ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) {
++ chunk = wrappedFull.getWrapped();
++ } else {
++ final ServerLevel world = this.world;
++ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
++ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
++ ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos
++ });
++ }
++
++ final NewChunkHolder chunkHolder = this.chunkHolder;
++
++ chunk.setFullStatus(chunkHolder::getChunkStatus);
++ chunk.runPostLoad();
++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla)
++ // This brings entity addition back in line with older versions of the game
++ // Since we load the NBT in the empty status, this will never block for I/O
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false);
++
++ // we don't need the entitiesInLevel, not sure why it's there
++ chunk.setLoaded(true);
++ chunk.registerAllBlockEntitiesAfterLevelLoad();
++ chunk.registerTickContainerInLevel(this.world);
++ } catch (final Throwable throwable) {
++ this.complete(null, throwable);
++ return;
++ }
++ this.complete(chunk, null);
++ }
++
++ protected volatile boolean scheduled;
++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class);
++
++ @Override
++ public boolean isScheduled() {
++ return this.scheduled;
++ }
++
++ @Override
++ public void schedule() {
++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) {
++ throw new IllegalStateException("Cannot double call schedule()");
++ }
++ this.convertToFullTask.queue();
++ }
++
++ @Override
++ public void cancel() {
++ if (this.convertToFullTask.cancel()) {
++ this.complete(null, null);
++ }
++ }
++
++ @Override
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.convertToFullTask.getPriority();
++ }
++
++ @Override
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.convertToFullTask.lowerPriority(priority);
++ }
++
++ @Override
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.convertToFullTask.setPriority(priority);
++ }
++
++ @Override
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.convertToFullTask.raisePriority(priority);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8a938922f
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
+@@ -0,0 +1,181 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder;
++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import org.apache.logging.log4j.LogManager;
++import org.apache.logging.log4j.Logger;
++import java.util.function.BooleanSupplier;
++
++public final class ChunkLightTask extends ChunkProgressionTask {
++
++ private static final Logger LOGGER = LogManager.getLogger();
++
++ private final ChunkAccess fromChunk;
++
++ private final LightTaskPriorityHolder priorityHolder;
++
++ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
++ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ);
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
++ this.fromChunk = chunk;
++ }
++
++ @Override
++ public boolean isScheduled() {
++ return this.priorityHolder.isScheduled();
++ }
++
++ @Override
++ public ChunkStatus getTargetStatus() {
++ return ChunkStatus.LIGHT;
++ }
++
++ @Override
++ public void schedule() {
++ this.priorityHolder.schedule();
++ }
++
++ @Override
++ public void cancel() {
++ this.priorityHolder.cancel();
++ }
++
++ @Override
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.priorityHolder.getPriority();
++ }
++
++ @Override
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ this.priorityHolder.raisePriority(priority);
++ }
++
++ @Override
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ this.priorityHolder.setPriority(priority);
++ }
++
++ @Override
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ this.priorityHolder.raisePriority(priority);
++ }
++
++ private static final class LightTaskPriorityHolder extends PriorityHolder {
++
++ private final ChunkLightTask task;
++
++ private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
++ super(priority);
++ this.task = task;
++ }
++
++ @Override
++ protected void cancelScheduled() {
++ final ChunkLightTask task = this.task;
++ task.complete(null, null);
++ }
++
++ @Override
++ protected PrioritisedExecutor.Priority getScheduledPriority() {
++ final ChunkLightTask task = this.task;
++ return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ);
++ }
++
++ @Override
++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) {
++ final ChunkLightTask task = this.task;
++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
++ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority);
++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
++ }
++
++ @Override
++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ final ChunkLightTask task = this.task;
++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
++ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority);
++ }
++
++ @Override
++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ final ChunkLightTask task = this.task;
++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
++ }
++
++ @Override
++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ final ChunkLightTask task = this.task;
++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
++ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority);
++ }
++ }
++
++ private static final class LightTask implements BooleanSupplier {
++
++ private final StarLightInterface lightEngine;
++ private final ChunkLightTask task;
++
++ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) {
++ this.lightEngine = lightEngine;
++ this.task = task;
++ }
++
++ @Override
++ public boolean getAsBoolean() {
++ final ChunkLightTask task = this.task;
++ // executed on light thread
++ if (!task.priorityHolder.markExecuting()) {
++ // cancelled
++ return false;
+ }
-+ case "chunkinfo" -> {
-+ List<String> worldNames = new ArrayList<>();
-+ worldNames.add("*");
-+ for (org.bukkit.World world : Bukkit.getWorlds()) {
-+ worldNames.add(world.getName());
++
++ try {
++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk);
++
++ if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
++ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections);
++ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ);
++ } else {
++ task.fromChunk.setLightCorrect(false);
++ this.lightEngine.lightChunk(task.fromChunk, emptySections);
++ task.fromChunk.setLightCorrect(true);
+ }
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
++ // we need to advance status
++ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) {
++ chunk.setPersistedStatus(ChunkStatus.LIGHT);
+ }
++ } catch (final Throwable thr) {
++ LOGGER.fatal(
++ "Failed to light chunk " + task.fromChunk.getPos().toString()
++ + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr
++ );
++
++ task.complete(null, thr);
++
++ return true;
+ }
++
++ task.complete(task.fromChunk, null);
++ return true;
+ }
-+ return Collections.emptyList();
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13091425f8
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
+@@ -0,0 +1,487 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
-+ private void doChunkInfo(final CommandSender sender, final String[] args) {
-+ List<org.bukkit.World> worlds;
-+ if (args.length < 1 || args[0].equals("*")) {
-+ worlds = Bukkit.getWorlds();
-+ } else {
-+ worlds = new ArrayList<>(args.length);
-+ for (final String arg : args) {
-+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
-+ if (world == null) {
-+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
-+ return;
-+ }
-+ worlds.add(world);
-+ }
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters;
++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.UpgradeData;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.chunk.storage.ChunkSerializer;
++import net.minecraft.world.level.levelgen.blending.BlendingData;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.function.Consumer;
++
++public final class ChunkLoadTask extends ChunkProgressionTask {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class);
++
++ private final NewChunkHolder chunkHolder;
++ private final ChunkDataLoadTask loadTask;
++
++ private volatile boolean cancelled;
++ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
++ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
++ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult;
++ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data
++
++ public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
++ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ);
++ this.chunkHolder = chunkHolder;
++ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
++ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> {
++ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
++ ChunkLoadTask.this.tryCompleteLoad();
++ });
++ }
++
++ private void tryCompleteLoad() {
++ final int count = this.taskCountToComplete.decrementAndGet();
++ if (count == 0) {
++ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement
++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
++ } else if (count < 0) {
++ throw new IllegalStateException("Called tryCompleteLoad() too many times");
+ }
++ }
+
-+ int accumulatedTotal = 0;
-+ int accumulatedInactive = 0;
-+ int accumulatedBorder = 0;
-+ int accumulatedTicking = 0;
-+ int accumulatedEntityTicking = 0;
++ @Override
++ public ChunkStatus getTargetStatus() {
++ return ChunkStatus.EMPTY;
++ }
+
-+ for (final org.bukkit.World bukkitWorld : worlds) {
-+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
++ private boolean scheduled;
+
-+ int total = 0;
-+ int inactive = 0;
-+ int full = 0;
-+ int blockTicking = 0;
-+ int entityTicking = 0;
++ @Override
++ public boolean isScheduled() {
++ return this.scheduled;
++ }
+
-+ for (final ChunkHolder chunk : io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(world)) {
-+ if (chunk.getFullChunkNowUnchecked() == null) {
-+ continue;
-+ }
++ @Override
++ public void schedule() {
++ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
++ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
+
-+ ++total;
++ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> {
++ ChunkLoadTask.this.tryCompleteLoad();
++ };
+
-+ FullChunkStatus state = chunk.getFullStatus();
++ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because
++ // they must schedule a task to off main or to on main to complete
++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
++ try {
++ if (this.scheduled) {
++ throw new IllegalStateException("schedule() called twice");
++ }
++ this.scheduled = true;
++ if (this.cancelled) {
++ return;
++ }
++ if (!this.chunkHolder.isEntityChunkNBTLoaded()) {
++ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask);
++ } else {
++ entityLoadTask = null;
++ this.tryCompleteLoad();
++ }
+
-+ switch (state) {
-+ case INACCESSIBLE -> ++inactive;
-+ case FULL -> ++full;
-+ case BLOCK_TICKING -> ++blockTicking;
-+ case ENTITY_TICKING -> ++entityTicking;
-+ }
++ if (!this.chunkHolder.isPoiChunkLoaded()) {
++ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask);
++ } else {
++ poiLoadTask = null;
++ this.tryCompleteLoad();
+ }
+
-+ accumulatedTotal += total;
-+ accumulatedInactive += inactive;
-+ accumulatedBorder += full;
-+ accumulatedTicking += blockTicking;
-+ accumulatedEntityTicking += entityTicking;
++ this.entityLoadTask = entityLoadTask;
++ this.poiLoadTask = poiLoadTask;
++ } finally {
++ this.scheduler.schedulingLockArea.unlock(schedulingLock);
++ }
+
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(total),
-+ text(" Inactive: ", BLUE), text(inactive),
-+ text(" Full: ", BLUE), text(full),
-+ text(" Block Ticking: ", BLUE), text(blockTicking),
-+ text(" Entity Ticking: ", BLUE), text(entityTicking)
-+ ));
++ if (entityLoadTask != null) {
++ entityLoadTask.schedule();
+ }
-+ if (worlds.size() > 1) {
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(accumulatedTotal),
-+ text(" Inactive: ", BLUE), text(accumulatedInactive),
-+ text(" Full: ", BLUE), text(accumulatedBorder),
-+ text(" Block Ticking: ", BLUE), text(accumulatedTicking),
-+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking)
-+ ));
++
++ if (poiLoadTask != null) {
++ poiLoadTask.schedule();
+ }
++
++ this.loadTask.schedule(false);
+ }
+
-+ private void doHolderInfo(final CommandSender sender, final String[] args) {
-+ List<org.bukkit.World> worlds;
-+ if (args.length < 1 || args[0].equals("*")) {
-+ worlds = Bukkit.getWorlds();
-+ } else {
-+ worlds = new ArrayList<>(args.length);
-+ for (final String arg : args) {
-+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
-+ if (world == null) {
-+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
-+ return;
++ @Override
++ public void cancel() {
++ // must be before load task access, so we can synchronise with the writes to the fields
++ final boolean scheduled;
++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
++ try {
++ // must read field here, as it may be written later conucrrently -
++ // we need to know if we scheduled _before_ cancellation
++ scheduled = this.scheduled;
++ this.cancelled = true;
++ } finally {
++ this.scheduler.schedulingLockArea.unlock(schedulingLock);
++ }
++
++ /*
++ Note: The entityLoadTask/poiLoadTask do not complete when cancelled,
++ so we need to manually try to complete in those cases
++ It is also important to note that we set the cancelled field first, just in case
++ the chunk load task attempts to complete with a non-null value
++ */
++
++ if (scheduled) {
++ // since we scheduled, we need to cancel the tasks
++ if (this.entityLoadTask != null) {
++ if (this.entityLoadTask.cancel()) {
++ this.tryCompleteLoad();
+ }
-+ worlds.add(world);
+ }
++ if (this.poiLoadTask != null) {
++ if (this.poiLoadTask.cancel()) {
++ this.tryCompleteLoad();
++ }
++ }
++ } else {
++ // since nothing was scheduled, we need to decrement the task count here ourselves
++
++ // for entity load task
++ this.tryCompleteLoad();
++
++ // for poi load task
++ this.tryCompleteLoad();
+ }
++ this.loadTask.cancel();
++ }
+
-+ int accumulatedTotal = 0;
-+ int accumulatedCanUnload = 0;
-+ int accumulatedNull = 0;
-+ int accumulatedReadOnly = 0;
-+ int accumulatedProtoChunk = 0;
-+ int accumulatedFullChunk = 0;
++ @Override
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.loadTask.getPriority();
++ }
+
-+ for (final org.bukkit.World bukkitWorld : worlds) {
-+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
++ @Override
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
++ if (entityLoad != null) {
++ entityLoad.lowerPriority(priority);
++ }
+
-+ int total = 0;
-+ int canUnload = 0;
-+ int nullChunks = 0;
-+ int readOnly = 0;
-+ int protoChunk = 0;
-+ int fullChunk = 0;
++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
-+ for (final ChunkHolder chunk : world.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders()) { // Paper - change updating chunks map
-+ final ChunkAccess lastChunk = chunk.getAvailableChunkNow();
++ if (poiLoad != null) {
++ poiLoad.lowerPriority(priority);
++ }
+
-+ ++total;
++ this.loadTask.lowerPriority(priority);
++ }
+
-+ if (lastChunk == null) {
-+ ++nullChunks;
-+ } else if (lastChunk instanceof ImposterProtoChunk) {
-+ ++readOnly;
-+ } else if (lastChunk instanceof ProtoChunk) {
-+ ++protoChunk;
-+ } else if (lastChunk instanceof LevelChunk) {
-+ ++fullChunk;
-+ }
++ @Override
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
++ if (entityLoad != null) {
++ entityLoad.setPriority(priority);
++ }
+
-+ if (chunk.newChunkHolder.isSafeToUnload() == null) {
-+ ++canUnload;
-+ }
-+ }
++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
-+ accumulatedTotal += total;
-+ accumulatedCanUnload += canUnload;
-+ accumulatedNull += nullChunks;
-+ accumulatedReadOnly += readOnly;
-+ accumulatedProtoChunk += protoChunk;
-+ accumulatedFullChunk += fullChunk;
++ if (poiLoad != null) {
++ poiLoad.setPriority(priority);
++ }
+
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(total),
-+ text(" Unloadable: ", BLUE), text(canUnload),
-+ text(" Null: ", BLUE), text(nullChunks),
-+ text(" ReadOnly: ", BLUE), text(readOnly),
-+ text(" Proto: ", BLUE), text(protoChunk),
-+ text(" Full: ", BLUE), text(fullChunk)
-+ ));
++ this.loadTask.setPriority(priority);
++ }
++
++ @Override
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
++ if (entityLoad != null) {
++ entityLoad.raisePriority(priority);
+ }
-+ if (worlds.size() > 1) {
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(accumulatedTotal),
-+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload),
-+ text(" Null: ", BLUE), text(accumulatedNull),
-+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly),
-+ text(" Proto: ", BLUE), text(accumulatedProtoChunk),
-+ text(" Full: ", BLUE), text(accumulatedFullChunk)
-+ ));
++
++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
++
++ if (poiLoad != null) {
++ poiLoad.raisePriority(priority);
+ }
++
++ this.loadTask.raisePriority(priority);
+ }
+
-+ private void doDebug(final CommandSender sender, final String[] args) {
-+ if (args.length < 1) {
-+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
-+ return;
++ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> {
++
++ private TaskResult<FinalCompletion, Throwable> result;
++ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>();
++
++ protected volatile boolean completed;
++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class);
++
++ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final RegionFileIOThread.RegionFileType type,
++ final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, type, priority);
+ }
+
-+ final String debugType = args[0].toLowerCase(Locale.ROOT);
-+ switch (debugType) {
-+ case "chunks" -> {
-+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) {
-+ sender.sendMessage(text("Use /paper debug chunks [world] to dump loaded chunk information to a file", RED));
-+ break;
++ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) {
++ if (!this.waiters.add(consumer)) {
++ try {
++ consumer.accept(this.result);
++ } catch (final Throwable throwable) {
++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()),
++ "CallbackDataLoadTask impl", this.getClass().getName()
++ ), throwable);
+ }
-+ File file = new File(new File(new File("."), "debug"),
-+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
-+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN));
++ }
++ }
++
++ @Override
++ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) {
++ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) {
++ throw new IllegalStateException("Already completed");
++ }
++ this.result = result;
++ Consumer<TaskResult<FinalCompletion, Throwable>> consumer;
++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
+ try {
-+ MCUtil.dumpChunks(file, false);
-+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
-+ } catch (Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
++ consumer.accept(result);
++ } catch (final Throwable throwable) {
++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
++ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()),
++ "CallbackDataLoadTask impl", this.getClass().getName()
++ ), throwable);
++ return;
+ }
+ }
-+ // "help" & default
-+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ }
+ }
+
-+}
-diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-index 4de88f74182bb596c6b5ad0351cc0dacefd0ce96..2874bc3001c4e7d9191e47ba512c5a68369c21f1 100644
---- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart {
- public static GlobalConfiguration get() {
- return instance;
- }
++ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> {
++ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
++ }
+
-+ public ChunkLoadingBasic chunkLoadingBasic;
++ @Override
++ protected boolean hasOffMain() {
++ return true;
++ }
+
-+ public class ChunkLoadingBasic extends ConfigurationPart {
-+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
-+ public double playerMaxChunkSendRate = 75.0;
++ @Override
++ protected boolean hasOnMain() {
++ return true;
++ }
+
-+ @Comment(
-+ "The maximum rate at which chunks will load for any individual player. " +
-+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
-+ "chunk is already generated. Set to -1 to disable this limit."
-+ )
-+ public double playerMaxChunkLoadRate = 100.0;
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ return this.scheduler.loadExecutor.createTask(run, priority);
++ }
+
-+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
-+ public double playerMaxChunkGenerateRate = -1.0;
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority);
++ }
++
++ @Override
++ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
++ if (throwable != null) {
++ return new TaskResult<>(null, throwable);
++ }
++ if (data == null) {
++ return new TaskResult<>(this.getEmptyChunk(), null);
++ }
++
++ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
++ return this.deserialize(data);
++ }
++ // need to deserialize on main thread
++ return null;
++ }
++
++ private ProtoChunk getEmptyChunk() {
++ return new ProtoChunk(
++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world,
++ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null
++ );
++ }
++
++ @Override
++ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
++ if (throwable != null) {
++ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable);
++ return new TaskResult<>(null, null);
++ }
++
++ if (data == null) {
++ return new TaskResult<>(null, null);
++ }
++
++ try {
++ // run converters
++ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ));
++
++ return new TaskResult<>(converted, null);
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
++ return new TaskResult<>(null, null);
++ }
++ }
++
++ private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) {
++ try {
++ final ChunkAccess deserialized = ChunkSerializer.read(
++ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data
++ );
++ return new TaskResult<>(deserialized, null);
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
++ return new TaskResult<>(this.getEmptyChunk(), null);
++ }
++ }
++
++ @Override
++ protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
++ // data != null && throwable == null
++ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
++ throw new UnsupportedOperationException();
++ }
++ return this.deserialize(data);
++ }
+ }
+
-+ public ChunkLoadingAdvanced chunkLoadingAdvanced;
++ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> {
+
-+ public class ChunkLoadingAdvanced extends ConfigurationPart {
-+ @Comment(
-+ "Set to true if the server will match the chunk send radius that clients have configured" +
-+ "in their view distance settings if the client is less-than the server's send distance."
-+ )
-+ public boolean autoConfigSendDistance = true;
++ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority);
++ }
+
-+ @Comment(
-+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
-+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
-+ )
-+ public int playerMaxConcurrentChunkLoads = 0;
++ @Override
++ protected boolean hasOffMain() {
++ return true;
++ }
+
-+ @Comment(
-+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
-+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
-+ )
-+ public int playerMaxConcurrentChunkGenerates = 0;
++ @Override
++ protected boolean hasOnMain() {
++ return false;
++ }
++
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ return this.scheduler.loadExecutor.createTask(run, priority);
++ }
++
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
++ if (throwable != null) {
++ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable);
++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
++ }
++
++ if (data == null || data.isEmpty()) {
++ // nothing to do
++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
++ }
++
++ try {
++ // run converters
++ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world);
++
++ // now we need to parse it
++ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null);
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2);
++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
++ }
++ }
++
++ @Override
++ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) {
++ throw new UnsupportedOperationException();
++ }
+ }
- static void set(GlobalConfiguration instance) {
- GlobalConfiguration.instance = instance;
- }
-@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart {
- public int incomingPacketThreshold = 300;
- }
-
-- public ChunkLoading chunkLoading;
--
-- public class ChunkLoading extends ConfigurationPart {
-- public int minLoadRadius = 2;
-- public int maxConcurrentSends = 2;
-- public boolean autoconfigSendDistance = true;
-- public double targetPlayerChunkSendRate = 100.0;
-- public double globalMaxChunkSendRate = -1.0;
-- public boolean enableFrustumPriority = false;
-- public double globalMaxChunkLoadRate = -1.0;
-- public double playerMaxConcurrentLoads = 20.0;
-- public double globalMaxConcurrentLoads = 500.0;
-- public double playerMaxChunkLoadRate = -1.0;
-- }
--
- public UnsupportedSettings unsupportedSettings;
-
- public class UnsupportedSettings extends ConfigurationPart {
-@@ -201,7 +225,7 @@ public class GlobalConfiguration extends ConfigurationPart {
-
- @PostProcess
- private void postProcess() {
-- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
-+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
- }
- }
-
-diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
++
++ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
++
++ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority);
++ }
++
++ @Override
++ protected boolean hasOffMain() {
++ return true;
++ }
++
++ @Override
++ protected boolean hasOnMain() {
++ return false;
++ }
++
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ return this.scheduler.loadExecutor.createTask(run, priority);
++ }
++
++ @Override
++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
++ if (throwable != null) {
++ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable);
++ return new TaskResult<>(null, null);
++ }
++
++ if (data == null || data.isEmpty()) {
++ // nothing to do
++ return new TaskResult<>(null, null);
++ }
++
++ try {
++ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null);
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2);
++ return new TaskResult<>(null, thr2);
++ }
++ }
++
++ @Override
++ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
++ throw new UnsupportedOperationException();
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..d5d39e9c1f326e91010237b0db80d527ac52f4d6
+index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5df5361205
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
-@@ -0,0 +1,9 @@
-+package io.papermc.paper.threadedregions;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
+@@ -0,0 +1,101 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
-+// placeholder class for Folia
-+public class TickRegions {
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import java.lang.invoke.VarHandle;
++import java.util.Map;
++import java.util.function.BiConsumer;
+
-+ public static int getRegionChunkShift() {
-+ return 4;
++public abstract class ChunkProgressionTask {
++
++ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>();
++ private ChunkAccess completedChunk;
++ private Throwable completedThrowable;
++
++ protected final ChunkTaskScheduler scheduler;
++ protected final ServerLevel world;
++ protected final int chunkX;
++ protected final int chunkZ;
++
++ protected volatile boolean completed;
++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class);
++
++ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) {
++ this.scheduler = scheduler;
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
-index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644
---- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java
-+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
-@@ -2,6 +2,8 @@ package io.papermc.paper.util;
-
- public final class IntervalledCounter {
-
-+ private static final int INITIAL_SIZE = 8;
+
- protected long[] times;
- protected long[] counts;
- protected final long interval;
-@@ -11,8 +13,8 @@ public final class IntervalledCounter {
- protected int tail; // exclusive
-
- public IntervalledCounter(final long interval) {
-- this.times = new long[8];
-- this.counts = new long[8];
-+ this.times = new long[INITIAL_SIZE];
-+ this.counts = new long[INITIAL_SIZE];
- this.interval = interval;
- }
-
-@@ -67,13 +69,13 @@ public final class IntervalledCounter {
- this.tail = nextTail;
- }
-
-- public void updateAndAdd(final int count) {
-+ public void updateAndAdd(final long count) {
- final long currTime = System.nanoTime();
- this.updateCurrentTime(currTime);
- this.addTime(currTime, count);
- }
-
-- public void updateAndAdd(final int count, final long currTime) {
-+ public void updateAndAdd(final long count, final long currTime) {
- this.updateCurrentTime(currTime);
- this.addTime(currTime, count);
- }
-@@ -93,9 +95,13 @@ public final class IntervalledCounter {
- this.tail = size;
-
- if (tail >= head) {
-+ // sequentially ordered from [head, tail)
- System.arraycopy(oldElements, head, newElements, 0, size);
- System.arraycopy(oldCounts, head, newCounts, 0, size);
- } else {
-+ // ordered from [head, length)
-+ // then followed by [0, tail)
++ // Used only for debug json
++ public abstract boolean isScheduled();
+
- System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
- System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
-
-@@ -106,10 +112,18 @@ public final class IntervalledCounter {
-
- // returns in units per second
- public double getRate() {
-- return this.size() / (this.interval * 1.0e-9);
-+ return (double)this.sum / ((double)this.interval * 1.0E-9);
++ // Note: It is the responsibility of the task to set the chunk's status once it has completed
++ public abstract ChunkStatus getTargetStatus();
++
++ /* Only executed once */
++ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */
++ public abstract void schedule();
++
++ /* May be called multiple times */
++ public abstract void cancel();
++
++ public abstract PrioritisedExecutor.Priority getPriority();
++
++ /* Schedule lock is always held for the priority update calls */
++
++ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
++
++ public abstract void setPriority(final PrioritisedExecutor.Priority priority);
++
++ public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
++
++ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
++ if (!this.waiters.add(onComplete)) {
++ try {
++ onComplete.accept(this.completedChunk, this.completedThrowable);
++ } catch (final Throwable throwable) {
++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete),
++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable)
++ ), throwable);
++ }
++ }
+ }
+
-+ public long getInterval() {
-+ return this.interval;
- }
-
-- public long size() {
-+ public long getSum() {
- return this.sum;
- }
++ protected final void complete(final ChunkAccess chunk, final Throwable throwable) {
++ try {
++ this.complete0(chunk, throwable);
++ } catch (final Throwable thr2) {
++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable)
++ ), thr2);
++ }
++ }
++
++ private void complete0(final ChunkAccess chunk, final Throwable throwable) {
++ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) {
++ throw new IllegalStateException("Already completed");
++ }
++ this.completedChunk = chunk;
++ this.completedThrowable = throwable;
+
-+ public int totalDataPoints() {
-+ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
++ BiConsumer<ChunkAccess, Throwable> consumer;
++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
++ consumer.accept(chunk, throwable);
++ }
+ }
- }
-diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
-index c95a0af32178fe24448a5ae7a229c86ec883e8de..1d6b3fe2ce240af4ede61588795456b046eee6c9 100644
---- a/src/main/java/io/papermc/paper/util/MCUtil.java
-+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
-@@ -7,17 +7,30 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
- import io.papermc.paper.math.BlockPosition;
- import io.papermc.paper.math.FinePosition;
- import io.papermc.paper.math.Position;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonObject;
-+import com.google.gson.internal.Streams;
-+import com.google.gson.stream.JsonWriter;
-+import com.mojang.datafixers.util.Either;
- import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
- import java.lang.ref.Cleaner;
-+import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
- import net.minecraft.core.BlockPos;
- import net.minecraft.core.Direction;
- import net.minecraft.core.Vec3i;
- import net.minecraft.server.MinecraftServer;
++
++ @Override
++ public String toString() {
++ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) +
++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
++ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}";
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acbe954a8fa
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
+@@ -0,0 +1,217 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.DistanceManager;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.Ticket;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.ClipContext;
- import net.minecraft.world.level.Level;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerChunkCache;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.status.ChunkPyramid;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
- import net.minecraft.world.phys.Vec3;
- import org.apache.commons.lang.exception.ExceptionUtils;
- import com.mojang.authlib.GameProfile;
-@@ -30,8 +43,11 @@ import org.spigotmc.AsyncCatcher;
-
- import javax.annotation.Nonnull;
- import javax.annotation.Nullable;
-+import java.io.*;
-+import java.nio.charset.StandardCharsets;
- import java.util.List;
- import java.util.Queue;
-+import java.util.Set;
- import java.util.concurrent.CompletableFuture;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.LinkedBlockingQueue;
-@@ -532,6 +548,98 @@ public final class MCUtil {
- }
- }
-
-+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
-+ return chunk.getChunkHolderStatus();
++import net.minecraft.world.level.chunk.status.WorldGenContext;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.List;
++import java.util.Map;
++import java.util.concurrent.CompletableFuture;
++
++public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class);
++
++ private final ChunkAccess fromChunk;
++ private final ChunkStatus fromStatus;
++ private final ChunkStatus toStatus;
++ private final StaticCache2D<GenerationChunkHolder> neighbours;
++
++ private final PrioritisedExecutor.PrioritisedTask generateTask;
++
++ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours,
++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) {
++ super(scheduler, world, chunkX, chunkZ);
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.fromChunk = chunk;
++ this.fromStatus = chunk.getPersistedStatus();
++ this.toStatus = toStatus;
++ this.neighbours = neighbours;
++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) {
++ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority);
++ } else {
++ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius();
++ if (writeRadius < 0) {
++ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority);
++ } else {
++ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority);
++ }
++ }
+ }
+
-+ public static void dumpChunks(File file, boolean watchdog) throws IOException {
-+ file.getParentFile().mkdirs();
-+ file.createNewFile();
-+ ReferenceArrayList<org.bukkit.World> worlds = new ReferenceArrayList<>(org.bukkit.Bukkit.getWorlds());
-+ ReferenceArrayList<org.bukkit.World> loadedWorlds = new ReferenceArrayList<>(worlds);
-+ JsonObject data = new JsonObject();
++ @Override
++ public ChunkStatus getTargetStatus() {
++ return this.toStatus;
++ }
++
++ private boolean isEmptyTask() {
++ // must use fromStatus here to avoid any race condition with run() overwriting the status
++ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus);
++ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus());
++ }
+
-+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion());
-+ data.addProperty("data-version", 1);
++ @Override
++ public void run() {
++ final ChunkAccess chunk = this.fromChunk;
+
-+ {
-+ JsonArray players = new JsonArray();
-+ data.add("all-players", players);
-+ List<ServerPlayer> playerList = MinecraftServer.getServer().getPlayerList().players;
-+ for (ServerPlayer player : playerList) {
-+ JsonObject playerData = new JsonObject();
-+ players.add(playerData);
++ final ServerChunkCache serverChunkCache = this.world.getChunkSource();
++ final ChunkMap chunkMap = serverChunkCache.chunkMap;
+
-+ Level playerWorld = player.level();
-+ org.bukkit.World craftWorld = playerWorld.getWorld();
-+ Entity.RemovalReason removalReason = player.getRemovalReason();
++ final CompletableFuture<ChunkAccess> completeFuture;
+
-+ playerData.addProperty("name", player.getScoreboardName());
-+ playerData.addProperty("x", player.getX());
-+ playerData.addProperty("y", player.getY());
-+ playerData.addProperty("z", player.getZ());
-+ playerData.addProperty("world", playerWorld == null ? "null world" : craftWorld.getName());
-+ playerData.addProperty("removalReason", removalReason == null ? "null" : removalReason.name());
++ final boolean generation;
++ boolean completing = false;
++
++ // note: should optimise the case where the chunk does not need to execute the status, because
++ // schedule() calls this synchronously if it will run through that path
+
-+ if (!worlds.contains(craftWorld)) {
-+ worlds.add(craftWorld);
++ final WorldGenContext ctx = chunkMap.worldGenContext;
++ try {
++ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus);
++ if (generation) {
++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) {
++ if (chunk instanceof ProtoChunk) {
++ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus);
++ }
++ completing = true;
++ this.complete(chunk, null);
++ return;
+ }
++ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk)
++ .whenComplete((final ChunkAccess either, final Throwable throwable) -> {
++ if (either instanceof ProtoChunk proto) {
++ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus);
++ }
++ }
++ );
++ } else {
++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) {
++ completing = true;
++ this.complete(chunk, null);
++ return;
++ }
++ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk);
++ }
++ } catch (final Throwable throwable) {
++ if (!completing) {
++ this.complete(null, throwable);
++ return;
++ }
++
++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus),
++ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus),
++ "Generation task", this
++ ), throwable);
++
++ LOGGER.error(
++ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX +
++ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world),
++ throwable
++ );
++
++ return;
++ }
++
++ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) {
++ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation);
++ }
++
++ final ChunkAccess newChunk;
++
++ try {
++ newChunk = completeFuture.join();
++ } catch (final Throwable throwable) {
++ this.complete(null, throwable);
++ return;
++ }
++
++ if (newChunk == null) {
++ this.complete(null,
++ new IllegalStateException(
++ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString()
++ + ", generation: " + generation + " should not be null! Future: " + completeFuture
++ ).fillInStackTrace()
++ );
++ return;
++ }
++
++ this.complete(newChunk, null);
++ }
++
++ private volatile boolean scheduled;
++ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class);
++
++ @Override
++ public boolean isScheduled() {
++ return this.scheduled;
++ }
++
++ @Override
++ public void schedule() {
++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) {
++ throw new IllegalStateException("Cannot double call schedule()");
++ }
++ if (this.isEmptyTask()) {
++ if (this.generateTask.cancel()) {
++ this.run();
+ }
++ } else {
++ this.generateTask.queue();
++ }
++ }
++
++ @Override
++ public void cancel() {
++ if (this.generateTask.cancel()) {
++ this.complete(null, null);
+ }
++ }
++
++ @Override
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.generateTask.getPriority();
++ }
+
-+ JsonArray chunkWaitInformation = new JsonArray();
-+ data.add("chunk-wait-infos", chunkWaitInformation);
++ @Override
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.generateTask.lowerPriority(priority);
++ }
+
-+ for (io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.ChunkInfo chunkInfo : io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.getChunkInfos()) {
-+ chunkWaitInformation.add(chunkInfo.toString());
++ @Override
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
++ this.generateTask.setPriority(priority);
++ }
++
++ @Override
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.generateTask.raisePriority(priority);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be314b876c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
+@@ -0,0 +1,673 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
++
++import ca.spottedleaf.concurrentutil.completable.Completable;
++import ca.spottedleaf.concurrentutil.executor.Cancellable;
++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ServerLevel;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.function.BiConsumer;
++
++public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class);
++
++ protected static final CompoundTag CANCELLED_DATA = new CompoundTag();
++
++ // reference count is the upper 32 bits
++ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED);
++
++ protected static final long STAGE_MASK = 0xFFFFFFFFL;
++ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL;
++ protected static final long STAGE_NOT_STARTED = 0L;
++ protected static final long STAGE_LOADING = 1L;
++ protected static final long STAGE_PROCESSING = 2L;
++ protected static final long STAGE_COMPLETED = 3L;
++
++ // for loading data off disk
++ protected final LoadDataFromDiskTask loadDataFromDiskTask;
++ // processing off-main
++ protected final PrioritisedExecutor.PrioritisedTask processOffMain;
++ // processing on-main
++ protected final PrioritisedExecutor.PrioritisedTask processOnMain;
++
++ protected final ChunkTaskScheduler scheduler;
++ protected final ServerLevel world;
++ protected final int chunkX;
++ protected final int chunkZ;
++ protected final RegionFileIOThread.RegionFileType type;
++
++ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final RegionFileIOThread.RegionFileType type,
++ final PrioritisedExecutor.Priority priority) {
++ this.scheduler = scheduler;
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.type = type;
++
++ final ProcessOnMainTask mainTask;
++ if (this.hasOnMain()) {
++ mainTask = new ProcessOnMainTask();
++ this.processOnMain = this.createOnMain(mainTask, priority);
++ } else {
++ mainTask = null;
++ this.processOnMain = null;
++ }
++
++ final ProcessOffMainTask offMainTask;
++ if (this.hasOffMain()) {
++ offMainTask = new ProcessOffMainTask(mainTask);
++ this.processOffMain = this.createOffMain(offMainTask, priority);
++ } else {
++ offMainTask = null;
++ this.processOffMain = null;
++ }
++
++ if (this.processOffMain == null && this.processOnMain == null) {
++ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!");
++ }
++
++ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority);
++ }
++
++ public static final record TaskResult<L, R>(L left, R right) {}
++
++ protected abstract boolean hasOffMain();
++
++ protected abstract boolean hasOnMain();
++
++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
++
++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority);
++
++ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable);
++
++ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable);
++
++ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result);
++
++ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable);
++
++ @Override
++ public String toString() {
++ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) +
++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
++ ", type: " + this.type.toString() + "}";
++ }
++
++ public PrioritisedExecutor.Priority getPriority() {
++ if (this.processOnMain != null) {
++ return this.processOnMain.getPriority();
++ } else {
++ return this.processOffMain.getPriority();
++ }
++ }
++
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ // can't lower I/O tasks, we don't know what they affect
++ if (this.processOffMain != null) {
++ this.processOffMain.lowerPriority(priority);
++ }
++ if (this.processOnMain != null) {
++ this.processOnMain.lowerPriority(priority);
++ }
++ }
++
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ // can't lower I/O tasks, we don't know what they affect
++ this.loadDataFromDiskTask.raisePriority(priority);
++ if (this.processOffMain != null) {
++ this.processOffMain.setPriority(priority);
++ }
++ if (this.processOnMain != null) {
++ this.processOnMain.setPriority(priority);
++ }
++ }
++
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ // can't lower I/O tasks, we don't know what they affect
++ this.loadDataFromDiskTask.raisePriority(priority);
++ if (this.processOffMain != null) {
++ this.processOffMain.raisePriority(priority);
++ }
++ if (this.processOnMain != null) {
++ this.processOnMain.raisePriority(priority);
++ }
++ }
++
++ // returns whether scheduleNow() needs to be called
++ public boolean schedule(final boolean delay) {
++ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED ||
++ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) {
++ // try and increment reference count
++ int failures = 0;
++ for (long curr = this.stageAndReferenceCount.get();;) {
++ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) {
++ // cancelled or completed, nothing to do here
++ return false;
++ }
++
++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) {
++ // successful
++ return false;
++ }
+
-+ JsonArray worldsData = new JsonArray();
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
++ }
+
-+ for (org.bukkit.World bukkitWorld : worlds) {
-+ JsonObject worldData = new JsonObject();
++ if (!delay) {
++ this.scheduleNow();
++ return false;
++ }
++ return true;
++ }
+
-+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
-+ List<ServerPlayer> players = world.players();
++ public void scheduleNow() {
++ this.loadDataFromDiskTask.schedule(); // will schedule the rest
++ }
+
-+ worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld));
-+ worldData.addProperty("name", world.getWorld().getName());
-+ worldData.addProperty("view-distance", world.getWorld().getViewDistance()); // Paper - replace chunk loader system
-+ worldData.addProperty("tick-view-distance", world.getWorld().getSimulationDistance()); // Paper - replace chunk loader system
++ // assumes the current stage cannot be completed
++ // returns false if cancelled, returns true if can proceed
++ private boolean advanceStage(final long expect, final long to) {
++ int failures = 0;
++ for (long curr = this.stageAndReferenceCount.get();;) {
++ if ((curr & STAGE_MASK) != expect) {
++ // must be cancelled
++ return false;
++ }
+
-+ JsonArray playersData = new JsonArray();
++ final long newVal = (curr & ~STAGE_MASK) | to;
++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
++ return true;
++ }
+
-+ for (ServerPlayer player : players) {
-+ JsonObject playerData = new JsonObject();
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
++ }
+
-+ playerData.addProperty("name", player.getScoreboardName());
-+ playerData.addProperty("x", player.getX());
-+ playerData.addProperty("y", player.getY());
-+ playerData.addProperty("z", player.getZ());
++ public boolean cancel() {
++ int failures = 0;
++ for (long curr = this.stageAndReferenceCount.get();;) {
++ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) {
++ return false;
++ }
+
-+ playersData.add(playerData);
++ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) {
++ // no other references, so we can cancel
++ final long newVal = STAGE_CANCELLED;
++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
++ this.loadDataFromDiskTask.cancel();
++ if (this.processOffMain != null) {
++ this.processOffMain.cancel();
++ }
++ if (this.processOnMain != null) {
++ this.processOnMain.cancel();
++ }
++ this.onComplete(null);
++ return true;
++ }
++ } else {
++ if ((curr & ~STAGE_MASK) == (0L << 32)) {
++ throw new IllegalStateException("Reference count cannot be zero here");
++ }
++ // just decrease the reference count
++ final long newVal = curr - (1L << 32);
++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
++ return false;
++ }
+ }
+
-+ worldData.add("players", playersData);
-+ worldData.add("chunk-data", watchdog ? world.chunkTaskScheduler.chunkHolderManager.getDebugJsonForWatchdog() : world.chunkTaskScheduler.chunkHolderManager.getDebugJson());
-+ worldsData.add(worldData);
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
+ }
++ }
++
++ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> {
+
-+ data.add("worlds", worldsData);
++ private final ProcessOffMainTask offMainTask;
++ private final ProcessOnMainTask onMainTask;
+
-+ StringWriter stringWriter = new StringWriter();
-+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
-+ jsonWriter.setIndent(" ");
-+ jsonWriter.setLenient(false);
-+ Streams.write(data, jsonWriter);
++ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) {
++ this.offMainTask = offMainTask;
++ this.onMainTask = onMainTask;
++ }
++
++ @Override
++ public void accept(final CompoundTag compoundTag, final Throwable throwable) {
++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
++ // don't try to schedule further
++ return;
++ }
+
-+ String fileData = stringWriter.toString();
++ try {
++ if (compoundTag == CANCELLED_DATA) {
++ // cancelled, except this isn't possible
++ LOGGER.error("Data callback says cancelled, but stage does not?");
++ return;
++ }
+
-+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
-+ out.print(fileData);
++ // get off of the regionfile callback ASAP, no clue what locks are held right now...
++ if (GenericDataLoadTask.this.processOffMain != null) {
++ this.offMainTask.data = compoundTag;
++ this.offMainTask.throwable = throwable;
++ GenericDataLoadTask.this.processOffMain.queue();
++ return;
++ } else {
++ // no off-main task, so go straight to main
++ this.onMainTask.data = (OnMain)compoundTag;
++ this.onMainTask.throwable = throwable;
++ GenericDataLoadTask.this.processOnMain.queue();
++ }
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2);
++ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure(
++ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of(
++ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable)
++ ), thr2
++ );
++ }
+ }
+ }
+
- public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) {
- return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status);
- }
-diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
-index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..bdaf062f9b66ceab303a0807eca301342886a8ea 100644
---- a/src/main/java/io/papermc/paper/util/TickThread.java
-+++ b/src/main/java/io/papermc/paper/util/TickThread.java
-@@ -1,12 +1,20 @@
- package io.papermc.paper.util;
-
-+import net.minecraft.core.BlockPos;
- import net.minecraft.server.MinecraftServer;
- import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.network.ServerGamePacketListenerImpl;
-+import net.minecraft.util.Mth;
- import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
- import org.bukkit.Bukkit;
- import java.util.concurrent.atomic.AtomicInteger;
-
--public final class TickThread extends Thread {
-+public class TickThread extends Thread {
-
- public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks");
-
-@@ -16,6 +24,10 @@ public final class TickThread extends Thread {
- }
- }
-
-+ /**
-+ * @deprecated
-+ */
-+ @Deprecated
- public static void softEnsureTickThread(final String reason) {
- if (!STRICT_THREAD_CHECKS) {
- return;
-@@ -23,6 +35,10 @@ public final class TickThread extends Thread {
- ensureTickThread(reason);
- }
-
-+ /**
-+ * @deprecated
-+ */
-+ @Deprecated
- public static void ensureTickThread(final String reason) {
- if (!isTickThread()) {
- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-@@ -30,6 +46,20 @@ public final class TickThread extends Thread {
- }
- }
-
-+ public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) {
-+ if (!isTickThreadFor(world, pos)) {
-+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+ throw new IllegalStateException(reason);
++ private final class ProcessOffMainTask implements Runnable {
++
++ private CompoundTag data;
++ private Throwable throwable;
++ private final ProcessOnMainTask schedule;
++
++ public ProcessOffMainTask(final ProcessOnMainTask schedule) {
++ this.schedule = schedule;
++ }
++
++ @Override
++ public void run() {
++ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) {
++ // cancelled
++ return;
++ }
++ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable);
++
++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
++ // don't try to schedule further
++ return;
++ }
++
++ if (this.schedule != null) {
++ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right);
++
++ if (syncComplete != null) {
++ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
++ GenericDataLoadTask.this.onComplete(syncComplete);
++ } // else: cancelled
++ return;
++ }
++
++ this.schedule.data = newData.left;
++ this.schedule.throwable = newData.right;
++
++ GenericDataLoadTask.this.processOnMain.queue();
++ } else {
++ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData);
++ }
+ }
+ }
+
-+ public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) {
-+ if (!isTickThreadFor(world, pos)) {
-+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+ throw new IllegalStateException(reason);
++ private final class ProcessOnMainTask implements Runnable {
++
++ private OnMain data;
++ private Throwable throwable;
++
++ @Override
++ public void run() {
++ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
++ // cancelled
++ return;
++ }
++ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable);
++
++ GenericDataLoadTask.this.onComplete(result);
+ }
+ }
+
- public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
- if (!isTickThreadFor(world, chunkX, chunkZ)) {
- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-@@ -44,6 +74,20 @@ public final class TickThread extends Thread {
- }
- }
-
-+ public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) {
-+ if (!isTickThreadFor(world, aabb)) {
-+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+ throw new IllegalStateException(reason);
++ protected static final class LoadDataFromDiskTask {
++
++ private volatile int priority;
++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class);
++
++ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0;
++ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1;
++ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2;
++
++ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE;
++
++ private final int getPriorityVolatile() {
++ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this);
++ }
++
++ private final int compareAndExchangePriorityVolatile(final int expect, final int update) {
++ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update);
++ }
++
++ private final int getAndOrPriorityVolatile(final int val) {
++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val);
++ }
++
++ private final void setPriorityPlain(final int val) {
++ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val);
++ }
++
++ private final ServerLevel world;
++ private final int chunkX;
++ private final int chunkZ;
++
++ private final RegionFileIOThread.RegionFileType type;
++ private Cancellable dataLoadTask;
++ private Cancellable dataUnloadCancellable;
++ private DelayedPrioritisedTask dataUnloadTask;
++
++ private final BiConsumer<CompoundTag, Throwable> onComplete;
++ private final AtomicBoolean scheduled = new AtomicBoolean();
++
++ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does
++ // hold a priority lock.
++ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ,
++ final RegionFileIOThread.RegionFileType type,
++ final BiConsumer<CompoundTag, Throwable> onComplete,
++ final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.type = type;
++ this.onComplete = onComplete;
++ this.setPriorityPlain(priority.priority);
++ }
++
++ private void complete(final CompoundTag data, final Throwable throwable) {
++ try {
++ this.onComplete.accept(data, throwable);
++ } catch (final Throwable thr2) {
++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable),
++ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type)
++ ), thr2);
++ }
++ }
++
++ private boolean markExecuting() {
++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
++ }
++
++ private boolean isMarkedExecuted() {
++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
++ }
++
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++
++ int failures = 0;
++ for (int curr = this.getPriorityVolatile();;) {
++ if ((curr & PRIORITY_EXECUTED) != 0) {
++ // cancelled or executed
++ return;
++ }
++
++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
++ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ return;
++ }
++
++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
++ if (this.dataUnloadTask != null) {
++ this.dataUnloadTask.lowerPriority(priority);
++ }
++ // no return - we need to propagate priority
++ }
++
++ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) {
++ return;
++ }
++
++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
++ return;
++ }
++
++ // failed, retry
++
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
++ }
++
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++
++ int failures = 0;
++ for (int curr = this.getPriorityVolatile();;) {
++ if ((curr & PRIORITY_EXECUTED) != 0) {
++ // cancelled or executed
++ return;
++ }
++
++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ return;
++ }
++
++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
++ if (this.dataUnloadTask != null) {
++ this.dataUnloadTask.setPriority(priority);
++ }
++ // no return - we need to propagate priority
++ }
++
++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
++ return;
++ }
++
++ // failed, retry
++
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
++ }
++
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ throw new IllegalArgumentException("Invalid priority " + priority);
++ }
++
++ int failures = 0;
++ for (int curr = this.getPriorityVolatile();;) {
++ if ((curr & PRIORITY_EXECUTED) != 0) {
++ // cancelled or executed
++ return;
++ }
++
++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
++ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ return;
++ }
++
++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
++ if (this.dataUnloadTask != null) {
++ this.dataUnloadTask.raisePriority(priority);
++ }
++ // no return - we need to propagate priority
++ }
++
++ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) {
++ return;
++ }
++
++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
++ return;
++ }
++
++ // failed, retry
++
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
++ }
++
++ public void cancel() {
++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
++ // cancelled or executed already
++ return;
++ }
++
++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
++ // the write to dataLoadTask will check for the cancelled bit
++ if (this.dataUnloadCancellable != null) {
++ this.dataUnloadCancellable.cancel();
++ }
++
++ if (this.dataLoadTask != null) {
++ this.dataLoadTask.cancel();
++ }
++
++ this.complete(CANCELLED_DATA, null);
++ }
++
++ public void schedule() {
++ if (this.scheduled.getAndSet(true)) {
++ throw new IllegalStateException("schedule() called twice");
++ }
++ int priority = this.getPriorityVolatile();
++
++ if ((priority & PRIORITY_EXECUTED) != 0) {
++ // cancelled
++ return;
++ }
++
++ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
++ // to mark complete here to ensure we do not double complete
++ if (LoadDataFromDiskTask.this.markExecuting()) {
++ LoadDataFromDiskTask.this.complete(data, thr);
++ } // else: cancelled
++ };
++
++ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
++ boolean scheduledUnload = false;
++
++ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
++ if (holder != null) {
++ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> {
++ if (data != null) {
++ consumer.accept(data, null);
++ } else {
++ // need to schedule task
++ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
++ }
++ };
++ Cancellable unloadCancellable = null;
++ CompoundTag syncComplete = null;
++ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists
++ final Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
++ if (unloadCompletable != null) {
++ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
++ if (unloadCancellable == null) {
++ syncComplete = unloadCompletable.getResult();
++ }
++ }
++
++ if (syncComplete != null) {
++ consumer.accept(syncComplete, null);
++ return;
++ }
++
++ if (unloadCancellable != null) {
++ scheduledUnload = true;
++ this.dataUnloadCancellable = unloadCancellable;
++ this.dataUnloadTask = unloadTask.task();
++ }
++ }
++
++ this.schedule(scheduledUnload, consumer, initialPriority);
++ }
++
++ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
++ int priority = this.getPriorityVolatile();
++
++ if ((priority & PRIORITY_EXECUTED) != 0) {
++ // cancelled
++ return;
++ }
++
++ if (!scheduledUnload) {
++ this.dataLoadTask = RegionFileIOThread.loadDataAsync(
++ this.world, this.chunkX, this.chunkZ, this.type, consumer,
++ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
++ );
++ }
++
++ int failures = 0;
++ for (;;) {
++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) {
++ return;
++ }
++
++ if ((priority & PRIORITY_EXECUTED) != 0) {
++ // cancelled or executed
++ if (this.dataUnloadCancellable != null) {
++ this.dataUnloadCancellable.cancel();
++ }
++
++ if (this.dataLoadTask != null) {
++ this.dataLoadTask.cancel();
++ }
++ return;
++ }
++
++ if (scheduledUnload) {
++ if (this.dataUnloadTask != null) {
++ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
++ }
++ } else {
++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
++ }
++
++ ++failures;
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++ }
+ }
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..21c9562781b05adf3871e522fddb654d75f605ba
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
+@@ -0,0 +1,7 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.server;
+
-+ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
-+ if (!isTickThreadFor(world, blockX, blockZ)) {
-+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+ throw new IllegalStateException(reason);
++public interface ChunkSystemMinecraftServer {
++
++ public void moonrise$setChunkSystemCrash(final Throwable throwable);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.status;
++
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++
++public interface ChunkSystemChunkStep {
++
++ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.storage;
++
++import net.minecraft.world.level.chunk.storage.RegionFileStorage;
++
++public interface ChunkSystemChunkStorage {
++
++ public RegionFileStorage moonrise$getRegionStorage();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.ticket;
++
++public interface ChunkSystemTicket<T> {
++
++ public long moonrise$getRemoveDelay();
++
++ public void moonrise$setRemoveDelay(final long removeDelay);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.ticks;
++
++public interface ChunkSystemLevelChunkTicks {
++
++ public boolean moonrise$isDirty(final long tick);
++
++ public void moonrise$clearDirty();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
+@@ -0,0 +1,15 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.util;
++
++import net.minecraft.util.SortedArraySet;
++
++public interface ChunkSystemSortedArraySet<T> {
++
++ public SortedArraySet<T> moonrise$copy();
++
++ public Object[] moonrise$copyBackingArray();
++
++ public T moonrise$replace(final T object);
++
++ public T moonrise$removeAndGet(final T object);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..51c9ed3dbb1787afc8e123b4f8104d54ed7ace89
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
+@@ -0,0 +1,320 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.util;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.longs.LongArrayList;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import java.util.Arrays;
++import java.util.Objects;
++
++public class ParallelSearchRadiusIteration {
++
++ // expected that this list returns for a given radius, the set of chunks ordered
++ // by manhattan distance
++ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][];
++ static {
++ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
++ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
++ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
+ }
+ }
+
- public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
-
- private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
-@@ -66,13 +110,45 @@ public final class TickThread extends Thread {
- }
-
- public static boolean isTickThread() {
-- return Bukkit.isPrimaryThread();
-+ return Thread.currentThread() instanceof TickThread;
++ public static long[] getSearchIteration(final int radius) {
++ return SEARCH_RADIUS_ITERATION_LIST[radius];
+ }
+
-+ public static boolean isShutdownThread() {
-+ return false;
++ private static class CustomLongArray extends LongArrayList {
++
++ public CustomLongArray() {
++ super();
++ }
++
++ public CustomLongArray(final int expected) {
++ super(expected);
++ }
++
++ public boolean addAll(final CustomLongArray list) {
++ this.addElements(this.size, list.a, 0, list.size);
++ return list.size != 0;
++ }
++
++ public void addUnchecked(final long value) {
++ this.a[this.size++] = value;
++ }
++
++ public void forceSize(final int to) {
++ this.size = to;
++ }
++
++ @Override
++ public int hashCode() {
++ long h = 1L;
++
++ Objects.checkFromToIndex(0, this.size, this.a.length);
++
++ for (int i = 0; i < this.size; ++i) {
++ h = HashCommon.mix(h + this.a[i]);
++ }
++
++ return (int)h;
++ }
++
++ @Override
++ public boolean equals(final Object o) {
++ if (o == this) {
++ return true;
++ }
++
++ if (!(o instanceof CustomLongArray other)) {
++ return false;
++ }
++
++ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size);
++ }
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
-+ return isTickThread();
++ private static int getDistanceSize(final int radius, final int max) {
++ if (radius == 0) {
++ return 1;
++ }
++ final int diff = radius - max;
++ if (diff <= 0) {
++ return 4*radius;
++ }
++ return 4*(max - Math.max(0, diff - 1));
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
-+ return isTickThread();
++ private static int getQ1DistanceSize(final int radius, final int max) {
++ if (radius == 0) {
++ return 1;
++ }
++ final int diff = radius - max;
++ if (diff <= 0) {
++ return radius+1;
++ }
++ return max - diff + 1;
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
-+ return isTickThread();
- }
-
- public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
- return isTickThread();
- }
-
-+ public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) {
-+ return isTickThread();
++ private static final class BasicFIFOLQueue {
++
++ private final long[] values;
++ private int head, tail;
++
++ public BasicFIFOLQueue(final int cap) {
++ if (cap <= 1) {
++ throw new IllegalArgumentException();
++ }
++ this.values = new long[cap];
++ }
++
++ public boolean isEmpty() {
++ return this.head == this.tail;
++ }
++
++ public long removeFirst() {
++ final long ret = this.values[this.head];
++
++ if (this.head == this.tail) {
++ throw new IllegalStateException();
++ }
++
++ ++this.head;
++ if (this.head == this.values.length) {
++ this.head = 0;
++ }
++
++ return ret;
++ }
++
++ public void addLast(final long value) {
++ this.values[this.tail++] = value;
++
++ if (this.tail == this.head) {
++ throw new IllegalStateException();
++ }
++
++ if (this.tail == this.values.length) {
++ this.tail = 0;
++ }
++ }
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
-+ return isTickThread();
++ private static CustomLongArray[] makeQ1BFS(final int radius) {
++ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1];
++ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1);
++ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1));
++
++ seen.add(CoordinateUtils.getChunkKey(0, 0));
++ queue.addLast(CoordinateUtils.getChunkKey(0, 0));
++ while (!queue.isEmpty()) {
++ final long chunk = queue.removeFirst();
++ final int chunkX = CoordinateUtils.getChunkX(chunk);
++ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
++
++ final int index = Math.abs(chunkX) + Math.abs(chunkZ);
++ final CustomLongArray list = ret[index];
++ if (list != null) {
++ list.addUnchecked(chunk);
++ } else {
++ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk);
++ }
++
++ for (int i = 0; i < 4; ++i) {
++ // 0 -> -1, 0
++ // 1 -> 0, -1
++ // 2 -> 1, 0
++ // 3 -> 0, 1
++
++ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0)
++ // note: -n = (~n) + 1
++ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1)
++
++ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1
++ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0
++ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1
++
++ final int neighbourX = chunkX + dx;
++ final int neighbourZ = chunkZ + dz;
++ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
++
++ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
++ // don't enqueue out of range
++ continue;
++ }
++
++ if (!seen.add(neighbour)) {
++ continue;
++ }
++
++ queue.addLast(neighbour);
++ }
++ }
++
++ return ret;
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
-+ return isTickThread();
++ // doesn't appear worth optimising this function now, even though it's 70% of the call
++ private static CustomLongArray spread(final CustomLongArray input, final int size) {
++ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input);
++ final CustomLongArray added = new CustomLongArray(size);
++
++ while (!notAdded.isEmpty()) {
++ if (added.isEmpty()) {
++ added.addUnchecked(notAdded.removeLastLong());
++ continue;
++ }
++
++ long maxChunk = -1L;
++ int maxDist = 0;
++
++ // select the chunk from the not yet added set that has the largest minimum distance from
++ // the current set of added chunks
++
++ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) {
++ final long chunkKey = iterator.nextLong();
++ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
++
++ int minDist = Integer.MAX_VALUE;
++
++ final int len = added.size();
++ final long[] addedArr = added.elements();
++ Objects.checkFromToIndex(0, len, addedArr.length);
++ for (int i = 0; i < len; ++i) {
++ final long addedKey = addedArr[i];
++ final int addedX = CoordinateUtils.getChunkX(addedKey);
++ final int addedZ = CoordinateUtils.getChunkZ(addedKey);
++
++ // here we use square distance because chunk generation uses neighbours in a square radius
++ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ));
++
++ minDist = Math.min(dist, minDist);
++ }
++
++ if (minDist > maxDist) {
++ maxDist = minDist;
++ maxChunk = chunkKey;
++ }
++ }
++
++ // move the selected chunk from the not added set to the added set
++
++ if (!notAdded.remove(maxChunk)) {
++ throw new IllegalStateException();
++ }
++
++ added.addUnchecked(maxChunk);
++ }
++
++ return added;
+ }
+
-+ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
-+ return isTickThread();
++ private static void expandQuadrants(final CustomLongArray input, final int size) {
++ final int len = input.size();
++ final long[] array = input.elements();
++
++ int writeIndex = size - 1;
++ for (int i = len - 1; i >= 0; --i) {
++ final long key = array[i];
++ final int chunkX = CoordinateUtils.getChunkX(key);
++ final int chunkZ = CoordinateUtils.getChunkZ(key);
++
++ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) {
++ throw new IllegalStateException();
++ }
++
++ // Q4
++ if (chunkZ != 0) {
++ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ);
++ }
++ // Q3
++ if (chunkX != 0 && chunkZ != 0) {
++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ);
++ }
++ // Q2
++ if (chunkX != 0) {
++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ);
++ }
++
++ array[writeIndex--] = key;
++ }
++
++ input.forceSize(size);
++
++ if (writeIndex != -1) {
++ throw new IllegalStateException();
++ }
+ }
+
- public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
- return isTickThread();
- }
-diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
++ private static long[] generateBFSOrder(final int radius) {
++ // by using only the first quadrant, we can reduce the total element size by 4 when spreading
++ final CustomLongArray[] byDistance = makeQ1BFS(radius);
++
++ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating
++ // this also means we are minimising locality
++ // but, we need to maintain sorted order by manhatten distance
++
++ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other
++ for (int i = 0, len = byDistance.length; i < len; ++i) {
++ final CustomLongArray points = byDistance[i];
++ final int expectedSize = getDistanceSize(i, radius);
++
++ final CustomLongArray spread = spread(points, expectedSize);
++ // add in Q2, Q3, Q4
++ expandQuadrants(spread, expectedSize);
++
++ byDistance[i] = spread;
++ }
++
++ // now, rebuild the list so that it still maintains manhatten distance order
++ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1));
++
++ for (final CustomLongArray dist : byDistance) {
++ ret.addAll(dist);
++ }
++
++ return ret.elements();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c78cbec447032de9fe69748591bef6be300160ed
+index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
-@@ -0,0 +1,607 @@
-+package io.papermc.paper.world;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
+@@ -0,0 +1,12 @@
++package ca.spottedleaf.moonrise.patches.chunk_system.world;
+
-+import com.destroystokyo.paper.util.maplist.EntityList;
-+import io.papermc.paper.chunk.system.entity.EntityLookup;
-+import io.papermc.paper.util.TickThread;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.EntityType;
-+import net.minecraft.world.entity.boss.EnderDragonPart;
-+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
-+import net.minecraft.world.level.entity.Visibility;
+import net.minecraft.world.phys.AABB;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
-+import org.bukkit.event.entity.EntityRemoveEvent;
+
-+public final class ChunkEntitySlices {
++public interface ChunkSystemEntityGetter {
+
-+ protected final int minSection;
-+ protected final int maxSection;
-+ public final int chunkX;
-+ public final int chunkZ;
-+ protected final ServerLevel world;
++ public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2bfdf3721db9a45e36538d71cbefcb1d339e6c58
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.patches.starlight.blockstate;
+
-+ protected final EntityCollectionBySection allEntities;
-+ protected final EntityCollectionBySection hardCollidingEntities;
-+ protected final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
-+ protected final EntityList entities = new EntityList();
++public interface StarlightAbstractBlockState {
+
-+ public FullChunkStatus status;
++ public boolean starlight$isConditionallyFullOpaque();
+
-+ protected boolean isTransient;
++ public int starlight$getOpacityIfCached();
+
-+ public boolean isTransient() {
-+ return this.isTransient;
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
+@@ -0,0 +1,18 @@
++package ca.spottedleaf.moonrise.patches.starlight.chunk;
++
++import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
++
++public interface StarlightChunk {
++
++ public SWMRNibbleArray[] starlight$getBlockNibbles();
++ public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles);
++
++ public SWMRNibbleArray[] starlight$getSkyNibbles();
++ public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles);
++
++ public boolean[] starlight$getSkyEmptinessMap();
++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap);
++
++ public boolean[] starlight$getBlockEmptinessMap();
++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap);
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d638b213aeb
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
+@@ -0,0 +1,277 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
++
++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
++import net.minecraft.core.BlockPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.LightChunkGetter;
++import net.minecraft.world.level.chunk.PalettedContainer;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.phys.shapes.Shapes;
++import net.minecraft.world.phys.shapes.VoxelShape;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Set;
++
++public final class BlockStarLightEngine extends StarLightEngine {
++
++ public BlockStarLightEngine(final Level world) {
++ super(false, world);
+ }
+
-+ public void setTransient(final boolean value) {
-+ this.isTransient = value;
++ @Override
++ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
++ return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap();
+ }
+
-+ // TODO implement container search optimisations
++ @Override
++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
++ ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to);
++ }
+
-+ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final FullChunkStatus status,
-+ final int minSection, final int maxSection) { // inclusive, inclusive
-+ this.minSection = minSection;
-+ this.maxSection = maxSection;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.world = world;
++ @Override
++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
++ return ((StarlightChunk)chunk).starlight$getBlockNibbles();
++ }
+
-+ this.allEntities = new EntityCollectionBySection(this);
-+ this.hardCollidingEntities = new EntityCollectionBySection(this);
-+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
++ @Override
++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
++ ((StarlightChunk)chunk).starlight$setBlockNibbles(to);
++ }
+
-+ this.status = status;
++ @Override
++ protected boolean canUseChunk(final ChunkAccess chunk) {
++ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
-+ // Paper start - optimise CraftChunk#getEntities
-+ public org.bukkit.entity.Entity[] getChunkEntities() {
-+ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
-+ final Entity[] entities = this.entities.getRawData();
-+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
-+ final Entity entity = entities[i];
-+ if (entity == null) {
-+ continue;
-+ }
-+ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
-+ if (bukkit != null && bukkit.isValid()) {
-+ ret.add(bukkit);
++ @Override
++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (nibble != null) {
++ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
++ // because a block was removed - which can decrease light. with sky data, block breaking can only result
++ // in increases, and thus the existing sky block check will actually correctly propagate light through
++ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
++ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
++ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
++ // of vanilla data management we "hide" them.
++ nibble.setHidden();
++ }
++ }
++
++ @Override
++ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
++ return;
++ }
++
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (nibble == null) {
++ if (!initRemovedNibbles) {
++ throw new IllegalStateException();
++ } else {
++ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
+ }
++ } else {
++ nibble.setNonNull();
+ }
++ }
+
-+ return ret.toArray(new org.bukkit.entity.Entity[0]);
++ @Override
++ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
++ // blocks can change opacity
++ // blocks can change emitted light
++ // blocks can change direction of propagation
++
++ final int encodeOffset = this.coordinateOffset;
++ final int emittedMask = this.emittedLightMask;
++
++ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
++ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
++ final int emittedLevel = blockState.getLightEmission() & emittedMask;
++
++ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
++ // this accounts for change in emitted light that would cause an increase
++ if (emittedLevel != 0) {
++ this.appendToIncreaseQueue(
++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (emittedLevel & 0xFL) << (6 + 6 + 16)
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
++ );
++ }
++ // this also accounts for a change in emitted light that would cause a decrease
++ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
++ // as it checks all neighbours (even if current level is 0)
++ this.appendToDecreaseQueue(
++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (currentLevel & 0xFL) << (6 + 6 + 16)
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ // always keep sided transparent false here, new block might be conditionally transparent which would
++ // prevent us from decreasing sources in the directions where the new block is opaque
++ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
++ // catch that and fix it.
++ );
++ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
+ }
+
-+ public CompoundTag save() {
-+ final int len = this.entities.size();
-+ if (len == 0) {
-+ return null;
++ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
++
++ @Override
++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
++ final int expect) {
++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
++ int level = centerState.getLightEmission() & 0xF;
++
++ if (level >= (15 - 1) || level > expect) {
++ return level;
+ }
+
-+ final Entity[] rawData = this.entities.getRawData();
-+ final List<Entity> collectedEntities = new ArrayList<>(len);
-+ for (int i = 0; i < len; ++i) {
-+ final Entity entity = rawData[i];
-+ if (entity.shouldBeSaved()) {
-+ collectedEntities.add(entity);
++ final int sectionOffset = this.chunkSectionIndexOffset;
++ final BlockState conditionallyOpaqueState;
++ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
++
++ if (opacity == -1) {
++ this.recalcCenterPos.set(worldX, worldY, worldZ);
++ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
++ conditionallyOpaqueState = centerState;
++ } else {
++ conditionallyOpaqueState = null;
+ }
++ } else if (opacity >= 15) {
++ return level;
++ } else {
++ conditionallyOpaqueState = null;
+ }
++ opacity = Math.max(1, opacity);
+
-+ if (collectedEntities.isEmpty()) {
-+ return null;
++ for (final AxisDirection direction : AXIS_DIRECTIONS) {
++ final int offX = worldX + direction.x;
++ final int offY = worldY + direction.y;
++ final int offZ = worldZ + direction.z;
++
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++
++ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
++
++ if ((neighbourLevel - 1) <= level) {
++ // don't need to test transparency, we know it wont affect the result.
++ continue;
++ }
++
++ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
++ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
++ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
++ // we don't read the blockstate because most of the time this is false, so using the faster
++ // known transparency lookup results in a net win
++ this.recalcNeighbourPos.set(offX, offY, offZ);
++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
++ // not allowed to propagate
++ continue;
++ }
++ }
++
++ // passed transparency,
++
++ final int calculated = neighbourLevel - opacity;
++ level = Math.max(calculated, level);
++ if (level > expect) {
++ return level;
++ }
+ }
+
-+ return EntityStorage.saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), this.world);
++ return level;
+ }
+
-+ // returns true if this chunk has transient entities remaining
-+ public boolean unload() {
-+ final int len = this.entities.size();
-+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
++ @Override
++ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
++ for (final BlockPos pos : positions) {
++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
++ }
+
-+ for (int i = 0; i < len; ++i) {
-+ final Entity entity = collectedEntities[i];
-+ if (entity.isRemoved()) {
-+ // removed by us below
++ this.performLightDecrease(lightAccess);
++ }
++
++ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
++ final List<BlockPos> sources = new ArrayList<>();
++
++ final int offX = chunk.getPos().x << 4;
++ final int offZ = chunk.getPos().z << 4;
++
++ final LevelChunkSection[] sections = chunk.getSections();
++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
++ final LevelChunkSection section = sections[sectionY - this.minSection];
++ if (section == null || section.hasOnlyAir()) {
++ // no sources in empty sections
+ continue;
+ }
-+ if (entity.shouldBeSaved()) {
-+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
-+ if (entity.isVehicle()) {
-+ // we cannot assume that these entities are contained within this chunk, because entities can
-+ // desync - so we need to remove them all
-+ for (final Entity passenger : entity.getIndirectPassengers()) {
-+ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
-+ }
++ if (!section.maybeHas((final BlockState state) -> {
++ return state.getLightEmission() > 0;
++ })) {
++ // no light sources in palette
++ continue;
++ }
++ final PalettedContainer<BlockState> states = section.states;
++ final int offY = sectionY << 4;
++
++ for (int index = 0; index < (16 * 16 * 16); ++index) {
++ final BlockState state = states.get(index);
++ if (state.getLightEmission() <= 0) {
++ continue;
+ }
++
++ // index = x | (z << 4) | (y << 8)
++ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
+ }
+ }
+
-+ return this.entities.size() != 0;
++ return sources;
+ }
+
-+ private List<Entity> getAllEntities() {
-+ final int len = this.entities.size();
-+ if (len == 0) {
-+ return new ArrayList<>();
++ @Override
++ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
++ // setup sources
++ final int emittedMask = this.emittedLightMask;
++ final List<BlockPos> positions = this.getSources(lightAccess, chunk);
++ for (int i = 0, len = positions.size(); i < len; ++i) {
++ final BlockPos pos = positions.get(i);
++ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
++ final int emittedLight = blockState.getLightEmission() & emittedMask;
++
++ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
++ // some other source is brighter
++ continue;
++ }
++
++ this.appendToIncreaseQueue(
++ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (emittedLight & 0xFL) << (6 + 6 + 16)
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
++ );
++
++
++ // propagation wont set this for us
++ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
+ }
+
-+ final Entity[] rawData = this.entities.getRawData();
-+ final List<Entity> collectedEntities = new ArrayList<>(len);
-+ for (int i = 0; i < len; ++i) {
-+ collectedEntities.add(rawData[i]);
++ if (needsEdgeChecks) {
++ // not required to propagate here, but this will reduce the hit of the edge checks
++ this.performLightIncrease(lightAccess);
++
++ // verify neighbour edges
++ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
++ } else {
++ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
++
++ this.performLightIncrease(lightAccess);
+ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
+@@ -0,0 +1,440 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
+
-+ return collectedEntities;
++import net.minecraft.world.level.chunk.DataLayer;
++import java.util.ArrayDeque;
++import java.util.Arrays;
++
++// SWMR -> Single Writer Multi Reader Nibble Array
++public final class SWMRNibbleArray {
++
++ /*
++ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
++ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
++ * nibbles can be written to.
++ *
++ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
++ *
++ * Initialised nibble - Has light data.
++ */
++
++ protected static final int INIT_STATE_NULL = 0; // null
++ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
++ protected static final int INIT_STATE_INIT = 2; // initialised
++ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
++
++ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
++ // this allows us to maintain only 1 byte array when we're not updating
++ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
++
++ private static byte[] allocateBytes() {
++ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
++ if (inPool != null) {
++ return inPool;
++ }
++
++ return new byte[ARRAY_SIZE];
+ }
+
-+ public void callEntitiesLoadEvent() {
-+ CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
++ private static void freeBytes(final byte[] bytes) {
++ WORKING_BYTES_POOL.get().addFirst(bytes);
+ }
+
-+ public void callEntitiesUnloadEvent() {
-+ CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
++ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
++ if (nibble == null) {
++ return new SWMRNibbleArray(null, true);
++ } else if (nibble.isEmpty()) {
++ return new SWMRNibbleArray();
++ } else {
++ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
++ }
+ }
-+ // Paper end - optimise CraftChunk#getEntities
+
-+ public boolean isEmpty() {
-+ return this.entities.size() == 0;
++ protected int stateUpdating;
++ protected volatile int stateVisible;
++
++ protected byte[] storageUpdating;
++ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
++ protected volatile byte[] storageVisible;
++
++ public SWMRNibbleArray() {
++ this(null, false); // lazy init
+ }
+
-+ public void mergeInto(final ChunkEntitySlices slices) {
-+ final Entity[] entities = this.entities.getRawData();
-+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
-+ final Entity entity = entities[i];
-+ slices.addEntity(entity, entity.sectionY);
++ public SWMRNibbleArray(final byte[] bytes) {
++ this(bytes, false);
++ }
++
++ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
++ if (bytes != null && bytes.length != ARRAY_SIZE) {
++ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
+ }
++ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
++ this.storageUpdating = this.storageVisible = bytes;
+ }
+
-+ private boolean preventStatusUpdates;
-+ public boolean startPreventingStatusUpdates() {
-+ final boolean ret = this.preventStatusUpdates;
-+ this.preventStatusUpdates = true;
-+ return ret;
++ public SWMRNibbleArray(final byte[] bytes, final int state) {
++ if (bytes != null && bytes.length != ARRAY_SIZE) {
++ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
++ }
++ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
++ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
++ }
++ this.stateUpdating = this.stateVisible = state;
++ this.storageUpdating = this.storageVisible = bytes;
+ }
+
-+ public boolean isPreventingStatusUpdates() {
-+ return this.preventStatusUpdates;
++ @Override
++ public String toString() {
++ StringBuilder stringBuilder = new StringBuilder();
++ stringBuilder.append("State: ");
++ switch (this.stateVisible) {
++ case INIT_STATE_NULL:
++ stringBuilder.append("null");
++ break;
++ case INIT_STATE_UNINIT:
++ stringBuilder.append("uninitialised");
++ break;
++ case INIT_STATE_INIT:
++ stringBuilder.append("initialised");
++ break;
++ case INIT_STATE_HIDDEN:
++ stringBuilder.append("hidden");
++ break;
++ default:
++ stringBuilder.append("unknown");
++ break;
++ }
++ stringBuilder.append("\nData:\n");
++
++ final byte[] data = this.storageVisible;
++ if (data != null) {
++ for (int i = 0; i < 4096; ++i) {
++ // Copied from NibbleArray#toString
++ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
++
++ stringBuilder.append(Integer.toHexString(level));
++ if ((i & 15) == 15) {
++ stringBuilder.append("\n");
++ }
++
++ if ((i & 255) == 255) {
++ stringBuilder.append("\n");
++ }
++ }
++ } else {
++ stringBuilder.append("null");
++ }
++
++ return stringBuilder.toString();
+ }
+
-+ public void stopPreventingStatusUpdates(final boolean prev) {
-+ this.preventStatusUpdates = prev;
++ public SaveState getSaveState() {
++ synchronized (this) {
++ final int state = this.stateVisible;
++ final byte[] data = this.storageVisible;
++ if (state == INIT_STATE_NULL) {
++ return null;
++ }
++ if (state == INIT_STATE_UNINIT) {
++ return new SaveState(null, state);
++ }
++ final boolean zero = isAllZero(data);
++ if (zero) {
++ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
++ } else {
++ return new SaveState(data.clone(), state);
++ }
++ }
+ }
+
-+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) {
-+ this.status = status;
++ protected static boolean isAllZero(final byte[] data) {
++ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
++ byte whole = data[i << 4];
+
-+ final Entity[] entities = this.entities.getRawData();
++ for (int k = 1; k < (1 << 4); ++k) {
++ whole |= data[(i << 4) | k];
++ }
+
-+ for (int i = 0, size = this.entities.size(); i < size; ++i) {
-+ final Entity entity = entities[i];
++ if (whole != 0) {
++ return false;
++ }
++ }
+
-+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity);
-+ entity.chunkStatus = status;
-+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity);
++ return true;
++ }
+
-+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false);
++ // operation type: updating on src, updating on other
++ public void extrudeLower(final SWMRNibbleArray other) {
++ if (other.stateUpdating == INIT_STATE_NULL) {
++ throw new IllegalArgumentException();
++ }
++
++ if (other.storageUpdating == null) {
++ this.setUninitialised();
++ return;
++ }
++
++ final byte[] src = other.storageUpdating;
++ final byte[] into;
++
++ if (!this.updatingDirty) {
++ if (this.storageUpdating != null) {
++ into = this.storageUpdating = allocateBytes();
++ } else {
++ this.storageUpdating = into = allocateBytes();
++ this.stateUpdating = INIT_STATE_INIT;
++ }
++ this.updatingDirty = true;
++ } else {
++ into = this.storageUpdating;
++ }
++
++ final int start = 0;
++ final int end = (15 | (15 << 4)) >>> 1;
++
++ /* x | (z << 4) | (y << 8) */
++ for (int y = 0; y <= 15; ++y) {
++ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
+ }
+ }
+
-+ public boolean addEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.add(entity)) {
-+ return false;
++ // operation type: updating
++ public void setFull() {
++ if (this.stateUpdating != INIT_STATE_HIDDEN) {
++ this.stateUpdating = INIT_STATE_INIT;
+ }
-+ entity.chunkStatus = this.status;
-+ final int sectionIndex = chunkSection - this.minSection;
++ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
++ this.updatingDirty = true;
++ }
+
-+ this.allEntities.addEntity(entity, sectionIndex);
++ // operation type: updating
++ public void setZero() {
++ if (this.stateUpdating != INIT_STATE_HIDDEN) {
++ this.stateUpdating = INIT_STATE_INIT;
++ }
++ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
++ this.updatingDirty = true;
++ }
+
-+ if (entity.hardCollides()) {
-+ this.hardCollidingEntities.addEntity(entity, sectionIndex);
++ // operation type: updating
++ public void setNonNull() {
++ if (this.stateUpdating == INIT_STATE_HIDDEN) {
++ this.stateUpdating = INIT_STATE_INIT;
++ return;
+ }
++ if (this.stateUpdating != INIT_STATE_NULL) {
++ return;
++ }
++ this.stateUpdating = INIT_STATE_UNINIT;
++ }
+
-+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
-+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
++ // operation type: updating
++ public void setNull() {
++ this.stateUpdating = INIT_STATE_NULL;
++ if (this.updatingDirty && this.storageUpdating != null) {
++ freeBytes(this.storageUpdating);
++ }
++ this.storageUpdating = null;
++ this.updatingDirty = false;
++ }
+
-+ if (entry.getKey().isInstance(entity)) {
-+ entry.getValue().addEntity(entity, sectionIndex);
++ // operation type: updating
++ public void setUninitialised() {
++ this.stateUpdating = INIT_STATE_UNINIT;
++ if (this.storageUpdating != null && this.updatingDirty) {
++ freeBytes(this.storageUpdating);
++ }
++ this.storageUpdating = null;
++ this.updatingDirty = false;
++ }
++
++ // operation type: updating
++ public void setHidden() {
++ if (this.stateUpdating == INIT_STATE_HIDDEN) {
++ return;
++ }
++ if (this.stateUpdating != INIT_STATE_INIT) {
++ this.setNull();
++ } else {
++ this.stateUpdating = INIT_STATE_HIDDEN;
++ }
++ }
++
++ // operation type: updating
++ public boolean isDirty() {
++ return this.stateUpdating != this.stateVisible || this.updatingDirty;
++ }
++
++ // operation type: updating
++ public boolean isNullNibbleUpdating() {
++ return this.stateUpdating == INIT_STATE_NULL;
++ }
++
++ // operation type: visible
++ public boolean isNullNibbleVisible() {
++ return this.stateVisible == INIT_STATE_NULL;
++ }
++
++ // opeartion type: updating
++ public boolean isUninitialisedUpdating() {
++ return this.stateUpdating == INIT_STATE_UNINIT;
++ }
++
++ // operation type: visible
++ public boolean isUninitialisedVisible() {
++ return this.stateVisible == INIT_STATE_UNINIT;
++ }
++
++ // operation type: updating
++ public boolean isInitialisedUpdating() {
++ return this.stateUpdating == INIT_STATE_INIT;
++ }
++
++ // operation type: visible
++ public boolean isInitialisedVisible() {
++ return this.stateVisible == INIT_STATE_INIT;
++ }
++
++ // operation type: updating
++ public boolean isHiddenUpdating() {
++ return this.stateUpdating == INIT_STATE_HIDDEN;
++ }
++
++ // operation type: updating
++ public boolean isHiddenVisible() {
++ return this.stateVisible == INIT_STATE_HIDDEN;
++ }
++
++ // operation type: updating
++ protected void swapUpdatingAndMarkDirty() {
++ if (this.updatingDirty) {
++ return;
++ }
++
++ if (this.storageUpdating == null) {
++ this.storageUpdating = allocateBytes();
++ Arrays.fill(this.storageUpdating, (byte)0);
++ } else {
++ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
++ }
++
++ if (this.stateUpdating != INIT_STATE_HIDDEN) {
++ this.stateUpdating = INIT_STATE_INIT;
++ }
++ this.updatingDirty = true;
++ }
++
++ // operation type: updating
++ public boolean updateVisible() {
++ if (!this.isDirty()) {
++ return false;
++ }
++
++ synchronized (this) {
++ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
++ this.storageVisible = null;
++ } else {
++ if (this.storageVisible == null) {
++ this.storageVisible = this.storageUpdating.clone();
++ } else {
++ if (this.storageUpdating != this.storageVisible) {
++ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
++ }
++ }
++
++ if (this.storageUpdating != this.storageVisible) {
++ freeBytes(this.storageUpdating);
++ }
++ this.storageUpdating = this.storageVisible;
+ }
++ this.updatingDirty = false;
++ this.stateVisible = this.stateUpdating;
+ }
+
+ return true;
+ }
+
-+ public boolean removeEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.remove(entity)) {
++ // operation type: visible
++ public DataLayer toVanillaNibble() {
++ synchronized (this) {
++ switch (this.stateVisible) {
++ case INIT_STATE_HIDDEN:
++ case INIT_STATE_NULL:
++ return null;
++ case INIT_STATE_UNINIT:
++ return new DataLayer();
++ case INIT_STATE_INIT:
++ return new DataLayer(this.storageVisible.clone());
++ default:
++ throw new IllegalStateException();
++ }
++ }
++ }
++
++ /* x | (z << 4) | (y << 8) */
++
++ // operation type: updating
++ public int getUpdating(final int x, final int y, final int z) {
++ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
++ }
++
++ // operation type: updating
++ public int getUpdating(final int index) {
++ // indices range from 0 -> 4096
++ final byte[] bytes = this.storageUpdating;
++ if (bytes == null) {
++ return 0;
++ }
++ final byte value = bytes[index >>> 1];
++
++ // if we are an even index, we want lower 4 bits
++ // if we are an odd index, we want upper 4 bits
++ return ((value >>> ((index & 1) << 2)) & 0xF);
++ }
++
++ // operation type: visible
++ public int getVisible(final int x, final int y, final int z) {
++ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
++ }
++
++ // operation type: visible
++ public int getVisible(final int index) {
++ // indices range from 0 -> 4096
++ final byte[] visibleBytes = this.storageVisible;
++ if (visibleBytes == null) {
++ return 0;
++ }
++ final byte value = visibleBytes[index >>> 1];
++
++ // if we are an even index, we want lower 4 bits
++ // if we are an odd index, we want upper 4 bits
++ return ((value >>> ((index & 1) << 2)) & 0xF);
++ }
++
++ // operation type: updating
++ public void set(final int x, final int y, final int z, final int value) {
++ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
++ }
++
++ // operation type: updating
++ public void set(final int index, final int value) {
++ if (!this.updatingDirty) {
++ this.swapUpdatingAndMarkDirty();
++ }
++ final int shift = (index & 1) << 2;
++ final int i = index >>> 1;
++
++ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
++ }
++
++ public static final class SaveState {
++
++ public final byte[] data;
++ public final int state;
++
++ public SaveState(final byte[] data, final int state) {
++ this.data = data;
++ this.state = state;
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def568142a4
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
+@@ -0,0 +1,711 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
++
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
++import it.unimi.dsi.fastutil.shorts.ShortCollection;
++import it.unimi.dsi.fastutil.shorts.ShortIterator;
++import net.minecraft.core.BlockPos;
++import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.LightChunkGetter;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.phys.shapes.Shapes;
++import net.minecraft.world.phys.shapes.VoxelShape;
++import java.util.Arrays;
++import java.util.Set;
++
++public final class SkyStarLightEngine extends StarLightEngine {
++
++ /*
++ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
++
++ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
++
++ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
++ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
++ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
++ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
++ How do we solve this?
++
++ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
++ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
++ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
++ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
++ to see if any of its nibbles need to be de-initialised.
++
++ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
++ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
++
++ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
++ around those.
++ */
++
++ protected final int[] heightMapBlockChange = new int[16 * 16];
++ {
++ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
++ }
++
++ protected final boolean[] nullPropagationCheckCache;
++
++ public SkyStarLightEngine(final Level world) {
++ super(true, world);
++ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
++ }
++
++ @Override
++ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
++ return;
++ }
++ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (nibble == null) {
++ if (!initRemovedNibbles) {
++ throw new IllegalStateException();
++ } else {
++ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
++ }
++ }
++ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
++ }
++
++ @Override
++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (nibble != null) {
++ nibble.setNull();
++ }
++ }
++
++ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
++ if (!currNibble.isNullNibbleUpdating()) {
++ // already initialised
++ return;
++ }
++
++ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
++
++ // are we above this chunk's lowest empty section?
++ int lowestY = this.minLightSection - 1;
++ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
++ if (emptinessMap == null) {
++ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
++ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
++ if (current == null || current.hasOnlyAir()) {
++ continue;
++ }
++ } else {
++ if (emptinessMap[currY - this.minSection]) {
++ continue;
++ }
++ }
++
++ // should always be full lit here
++ lowestY = currY;
++ break;
++ }
++
++ if (chunkY > lowestY) {
++ // we need to set this one to full
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ nibble.setNonNull();
++ nibble.setFull();
++ return;
++ }
++
++ if (extrude) {
++ // this nibble is going to depend solely on the skylight data above it
++ // find first non-null data above (there does exist one, as we just found it above)
++ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
++ if (nibble != null && !nibble.isNullNibbleUpdating()) {
++ currNibble.setNonNull();
++ currNibble.extrudeLower(nibble);
++ break;
++ }
++ }
++ } else {
++ currNibble.setNonNull();
++ }
++ }
++
++ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
++ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
++ final SWMRNibbleArray nibble = this.nibbleCache[index];
++ if (nibble != null && nibble.isNullNibbleUpdating()) {
++ // stop propagation in these areas
++ this.nibbleCache[index] = null;
++ nibble.updateVisible();
++ }
++ }
++ }
++
++ // rets whether neighbours were init'd
++
++ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
++ final boolean extrudeInitialised) {
++ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
++ // non-null. Propagation to these neighbours is necessary.
++ // What makes this easy is we know none of these neighbours are non-empty (otherwise
++ // this nibble would be initialised). So, we don't have to initialise
++ // the neighbours in the full 1 radius, because there's no worry that any "paths"
++ // to the neighbours on this horizontal plane are blocked.
++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
+ return false;
+ }
-+ entity.chunkStatus = null;
-+ final int sectionIndex = chunkSection - this.minSection;
++ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
+
-+ this.allEntities.removeEntity(entity, sectionIndex);
++ // check horizontal neighbours
++ boolean needInitNeighbours = false;
++ neighbour_search:
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
++ if (nibble != null && !nibble.isNullNibbleUpdating()) {
++ needInitNeighbours = true;
++ break neighbour_search;
++ }
++ }
++ }
+
-+ if (entity.hardCollides()) {
-+ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
++ if (needInitNeighbours) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
++ }
++ }
+ }
+
-+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
-+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
++ return needInitNeighbours;
++ }
+
-+ if (entry.getKey().isInstance(entity)) {
-+ entry.getValue().removeEntity(entity, sectionIndex);
++ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
++ final int chunkX = worldX >> 4;
++ int chunkY = worldY >> 4;
++ final int chunkZ = worldZ >> 4;
++
++ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (nibble != null) {
++ return nibble.getUpdating(worldX, worldY, worldZ);
++ }
++
++ for (;;) {
++ if (++chunkY > this.maxLightSection) {
++ return 15;
++ }
++
++ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++
++ if (nibble != null) {
++ return nibble.getUpdating(worldX, 0, worldZ);
+ }
+ }
++ }
+
-+ return true;
++ @Override
++ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
++ return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
+ }
+
-+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
++ @Override
++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
++ ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to);
+ }
+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
++ @Override
++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
++ return ((StarlightChunk)chunk).starlight$getSkyNibbles();
+ }
+
-+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ this.allEntities.getEntities(except, box, into, predicate);
++ @Override
++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
++ ((StarlightChunk)chunk).starlight$setSkyNibbles(to);
+ }
+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate);
++ @Override
++ protected boolean canUseChunk(final ChunkAccess chunk) {
++ // can only use chunks for sky stuff if their sections have been init'd
++ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
-+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
-+ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
++ @Override
++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
++ final int toSection) {
++ Arrays.fill(this.nullPropagationCheckCache, false);
++ this.rewriteNibbleCacheForSkylight(chunk);
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
++ for (int y = toSection; y >= fromSection; --y) {
++ this.checkNullSection(chunkX, y, chunkZ, true);
++ }
+
-+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
-+ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
-+ if (sectionEntities == null) {
++ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
++ }
++
++ @Override
++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
++ Arrays.fill(this.nullPropagationCheckCache, false);
++ this.rewriteNibbleCacheForSkylight(chunk);
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
++ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
++ final int y = (int)iterator.nextShort();
++ this.checkNullSection(chunkX, y, chunkZ, true);
++ }
++
++ super.checkChunkEdges(lightAccess, chunk, sections);
++ }
++
++ @Override
++ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
++ // blocks can change opacity
++ // blocks can change direction of propagation
++
++ // same logic applies from BlockStarLightEngine#checkBlock
++
++ final int encodeOffset = this.coordinateOffset;
++
++ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
++
++ if (currentLevel == 15) {
++ // must re-propagate clobbered source
++ this.appendToIncreaseQueue(
++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (currentLevel & 0xFL) << (6 + 6 + 16)
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
++ );
++ } else {
++ this.setLightLevel(worldX, worldY, worldZ, 0);
++ }
++
++ this.appendToDecreaseQueue(
++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (currentLevel & 0xFL) << (6 + 6 + 16)
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ );
++ }
++
++ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
++
++ @Override
++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
++ final int expect) {
++ if (expect == 15) {
++ return expect;
++ }
++
++ final int sectionOffset = this.chunkSectionIndexOffset;
++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
++ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
++
++ final BlockState conditionallyOpaqueState;
++ if (opacity < 0) {
++ this.recalcCenterPos.set(worldX, worldY, worldZ);
++ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
++ conditionallyOpaqueState = centerState;
++ } else {
++ conditionallyOpaqueState = null;
++ }
++ } else {
++ conditionallyOpaqueState = null;
++ opacity = Math.max(1, opacity);
++ }
++
++ int level = 0;
++
++ for (final AxisDirection direction : AXIS_DIRECTIONS) {
++ final int offX = worldX + direction.x;
++ final int offY = worldY + direction.y;
++ final int offZ = worldZ + direction.z;
++
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++
++ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
++
++ if ((neighbourLevel - 1) <= level) {
++ // don't need to test transparency, we know it wont affect the result.
+ continue;
+ }
+
-+ final Entity[] storage = sectionEntities.storage;
++ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
+
-+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
++ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
++ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
++ // we don't read the blockstate because most of the time this is false, so using the faster
++ // known transparency lookup results in a net win
++ this.recalcNeighbourPos.set(offX, offY, offZ);
++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
++ // not allowed to propagate
++ continue;
++ }
++ }
+
-+ if (clazz.isInstance(entity)) {
-+ ret.addEntity(entity, sectionIndex);
++ final int calculated = neighbourLevel - opacity;
++ level = Math.max(calculated, level);
++ if (level > expect) {
++ return level;
++ }
++ }
++
++ return level;
++ }
++
++ @Override
++ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
++ this.rewriteNibbleCacheForSkylight(atChunk);
++ Arrays.fill(this.nullPropagationCheckCache, false);
++
++ final BlockGetter world = lightAccess.getLevel();
++ final int chunkX = atChunk.getPos().x;
++ final int chunkZ = atChunk.getPos().z;
++ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
++
++ // setup heightmap for changes
++ for (final BlockPos pos : positions) {
++ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
++ final int curr = this.heightMapBlockChange[index];
++ if (pos.getY() > curr) {
++ this.heightMapBlockChange[index] = pos.getY();
++ }
++ }
++
++ // note: light sets are delayed while processing skylight source changes due to how
++ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
++ // below nibbles are initialised they aren't reading from partially modified nibbles
++
++ // now we can recalculate the sources for the changed columns
++ for (int index = 0; index < (16 * 16); ++index) {
++ final int maxY = this.heightMapBlockChange[index];
++ if (maxY == Integer.MIN_VALUE) {
++ // not changed
++ continue;
++ }
++ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
++
++ final int columnX = (index & 15) | (chunkX << 4);
++ final int columnZ = (index >>> 4) | (chunkZ << 4);
++
++ // try and propagate from the above y
++ // delay light set until after processing all sources to setup
++ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
++
++ // maxPropagationY is now the highest block that could not be propagated to
++
++ // remove all sources below that are 15
++ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
++ final int encodeOffset = this.coordinateOffset;
++
++ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
++ // ensure section is checked
++ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
++
++ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
++ if ((currY & 15) == 15) {
++ // ensure section is checked
++ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
++ }
++
++ // ensure section below is always checked
++ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
++ if (nibble == null) {
++ // advance currY to the the top of the section below
++ currY = (currY) & (~15);
++ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
++ // end up there
++ continue;
++ }
++
++ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
++ break;
++ }
++
++ // delay light set until after processing all sources to setup
++ this.appendToDecreaseQueue(
++ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (15L << (6 + 6 + 16))
++ | (propagateDirection << (6 + 6 + 16 + 4))
++ // do not set transparent blocks for the same reason we don't in the checkBlock method
++ );
+ }
+ }
+ }
+
-+ return ret;
++ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
++ // immediate light value
++ this.processDelayedIncreases();
++ this.processDelayedDecreases();
++
++ for (final BlockPos pos : positions) {
++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
++ }
++
++ this.performLightDecrease(lightAccess);
+ }
+
-+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
-+ if (collection != null) {
-+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ protected final int[] heightMapGen = new int[32 * 32];
++
++ @Override
++ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
++ this.rewriteNibbleCacheForSkylight(chunk);
++ Arrays.fill(this.nullPropagationCheckCache, false);
++
++ final BlockGetter world = lightAccess.getLevel();
++ final ChunkPos chunkPos = chunk.getPos();
++ final int chunkX = chunkPos.x;
++ final int chunkZ = chunkPos.z;
++
++ final LevelChunkSection[] sections = chunk.getSections();
++
++ int highestNonEmptySection = this.maxSection;
++ while (highestNonEmptySection == (this.minSection - 1) ||
++ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
++ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
++ // try propagate FULL to neighbours
++
++ // check neighbours to see if we need to propagate into them
++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
++ final int neighbourX = chunkX + direction.x;
++ final int neighbourZ = chunkZ + direction.z;
++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
++ if (neighbourNibble == null) {
++ // unloaded neighbour
++ // most of the time we fall here
++ continue;
++ }
++
++ // it looks like we need to propagate into the neighbour
++
++ final int incX;
++ final int incZ;
++ final int startX;
++ final int startZ;
++
++ if (direction.x != 0) {
++ // x direction
++ incX = 0;
++ incZ = 1;
++
++ if (direction.x < 0) {
++ // negative
++ startX = chunkX << 4;
++ } else {
++ startX = chunkX << 4 | 15;
++ }
++ startZ = chunkZ << 4;
++ } else {
++ // z direction
++ incX = 1;
++ incZ = 0;
++
++ if (direction.z < 0) {
++ // negative
++ startZ = chunkZ << 4;
++ } else {
++ startZ = chunkZ << 4 | 15;
++ }
++ startX = chunkX << 4;
++ }
++
++ final int encodeOffset = this.coordinateOffset;
++ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
++
++ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
++ this.appendToIncreaseQueue(
++ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (15L << (6 + 6 + 16)) // we know we're at full lit here
++ | (propagateDirection << (6 + 6 + 16 + 4))
++ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
++ );
++ }
++ }
++ }
++
++ if (highestNonEmptySection-- == (this.minSection - 1)) {
++ break;
++ }
++ }
++
++ if (highestNonEmptySection >= this.minSection) {
++ // fill out our other sources
++ final int minX = chunkPos.x << 4;
++ final int maxX = chunkPos.x << 4 | 15;
++ final int minZ = chunkPos.z << 4;
++ final int maxZ = chunkPos.z << 4 | 15;
++ final int startY = highestNonEmptySection << 4 | 15;
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
++ }
++ }
++ } // else: apparently the chunk is empty
++
++ if (needsEdgeChecks) {
++ // not required to propagate here, but this will reduce the hit of the edge checks
++ this.performLightIncrease(lightAccess);
++
++ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
++ this.checkNullSection(chunkX, y, chunkZ, false);
++ }
++ // no need to rewrite the nibble cache again
++ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
+ } else {
-+ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz));
-+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
++ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
++ this.checkNullSection(chunkX, y, chunkZ, false);
++ }
++ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
++
++ this.performLightIncrease(lightAccess);
+ }
+ }
+
-+ protected static final class BasicEntityList<E extends Entity> {
++ protected final void processDelayedIncreases() {
++ // copied from performLightIncrease
++ final long[] queue = this.increaseQueue;
++ final int decodeOffsetX = -this.encodeOffsetX;
++ final int decodeOffsetY = -this.encodeOffsetY;
++ final int decodeOffsetZ = -this.encodeOffsetZ;
+
-+ protected static final Entity[] EMPTY = new Entity[0];
-+ protected static final int DEFAULT_CAPACITY = 4;
++ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
++ final long queueValue = queue[i];
+
-+ protected E[] storage;
-+ protected int size;
++ final int posX = ((int)queueValue & 63) + decodeOffsetX;
++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
+
-+ public BasicEntityList() {
-+ this(0);
++ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
+ }
++ }
+
-+ public BasicEntityList(final int cap) {
-+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
-+ }
++ protected final void processDelayedDecreases() {
++ // copied from performLightDecrease
++ final long[] queue = this.decreaseQueue;
++ final int decodeOffsetX = -this.encodeOffsetX;
++ final int decodeOffsetY = -this.encodeOffsetY;
++ final int decodeOffsetZ = -this.encodeOffsetZ;
+
-+ public boolean isEmpty() {
-+ return this.size == 0;
++ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
++ final long queueValue = queue[i];
++
++ final int posX = ((int)queueValue & 63) + decodeOffsetX;
++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
++
++ this.setLightLevel(posX, posY, posZ, 0);
+ }
++ }
+
-+ public int size() {
-+ return this.size;
++ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
++ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
++ // clobbering the light values will result in broken propagation)
++ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
++ final boolean extrudeInitialised, final boolean delayLightSet) {
++ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
++ final int encodeOffset = this.coordinateOffset;
++ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
++
++ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
++ return startY;
+ }
+
-+ private void resize() {
-+ if (this.storage == EMPTY) {
-+ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
++ // ensure this section is always checked
++ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
++
++ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
++
++ for (;startY >= (this.minLightSection << 4); --startY) {
++ if ((startY & 15) == 15) {
++ // ensure this section is always checked
++ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
++ }
++ final BlockState current = this.getBlockState(worldX, startY, worldZ);
++
++ final VoxelShape fromShape;
++ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) {
++ this.mutablePos2.set(worldX, startY + 1, worldZ);
++ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
++ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
++ // above wont let us propagate
++ break;
++ }
+ } else {
-+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
++ fromShape = Shapes.empty();
+ }
-+ }
+
-+ public void add(final E entity) {
-+ final int idx = this.size++;
-+ if (idx >= this.storage.length) {
-+ this.resize();
-+ this.storage[idx] = entity;
++ final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached();
++ // does light propagate from the top down?
++ if (opacityIfCached != -1) {
++ if (opacityIfCached != 0) {
++ // we cannot propagate 15 through this
++ break;
++ }
++ // most of the time it falls here.
++ // add to propagate
++ // light set delayed until we determine if this nibble section is null
++ this.appendToIncreaseQueue(
++ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (15L << (6 + 6 + 16)) // we know we're at full lit here
++ | (propagateDirection << (6 + 6 + 16 + 4))
++ );
+ } else {
-+ this.storage[idx] = entity;
++ mutablePos.set(worldX, startY, worldZ);
++ long flags = 0L;
++ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
++
++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
++ // can't propagate here, we're done on this column.
++ break;
++ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
++
++ final int opacity = current.getLightBlock(world, mutablePos);
++ if (opacity > 0) {
++ // let the queued value (if any) handle it from here.
++ break;
++ }
++
++ // light set delayed until we determine if this nibble section is null
++ this.appendToIncreaseQueue(
++ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | (15L << (6 + 6 + 16)) // we know we're at full lit here
++ | (propagateDirection << (6 + 6 + 16 + 4))
++ | flags
++ );
++ }
++
++ above = current;
++
++ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
++ // we skip empty sections here, as this is just an easy way of making sure the above block
++ // can propagate through air.
++
++ // nothing can propagate in null sections, remove the queue entry for it
++ --this.increaseQueueInitialLength;
++
++ // advance currY to the the top of the section below
++ startY = (startY) & (~15);
++ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
++ // end up there
++
++ // make sure this is marked as AIR
++ above = AIR_BLOCK_STATE;
++ } else if (!delayLightSet) {
++ this.setLightLevel(worldX, startY, worldZ, 15);
+ }
+ }
+
-+ public int indexOf(final E entity) {
-+ final E[] storage = this.storage;
++ return startY;
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017c58191e2
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
+@@ -0,0 +1,1573 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
++
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
++import it.unimi.dsi.fastutil.shorts.ShortCollection;
++import it.unimi.dsi.fastutil.shorts.ShortIterator;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Direction;
++import net.minecraft.core.SectionPos;
++import net.minecraft.world.level.BlockGetter;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelHeightAccessor;
++import net.minecraft.world.level.LightLayer;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.LightChunkGetter;
++import net.minecraft.world.phys.shapes.Shapes;
++import net.minecraft.world.phys.shapes.VoxelShape;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.List;
++import java.util.Set;
++import java.util.function.Consumer;
++import java.util.function.IntConsumer;
+
-+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
-+ if (storage[i] == entity) {
-+ return i;
++public abstract class StarLightEngine {
++
++ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
++
++ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
++ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
++ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
++ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
++ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
++ };
++
++ protected static enum AxisDirection {
++
++ // Declaration order is important and relied upon. Do not change without modifying propagation code.
++ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
++ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
++ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
++
++ static {
++ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
++ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
++ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
++ }
++
++ protected AxisDirection opposite;
++
++ public final int x;
++ public final int y;
++ public final int z;
++ public final Direction nms;
++ public final long everythingButThisDirection;
++ public final long everythingButTheOppositeDirection;
++
++ AxisDirection(final int x, final int y, final int z) {
++ this.x = x;
++ this.y = y;
++ this.z = z;
++ this.nms = Direction.fromDelta(x, y, z);
++ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
++ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
++ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
++ }
++
++ public AxisDirection getOpposite() {
++ return this.opposite;
++ }
++ }
++
++ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
++ // for explaining how light propagates via breadth-first search
++
++ // While the above is a good start to understanding the general idea of what the general principles are, it's not
++ // exactly how the vanilla light engine should behave for minecraft.
++
++ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
++ // index = x + (z * 5) + (y * 25)
++ // null index indicates the chunk section doesn't exist (empty or out of bounds)
++ protected final LevelChunkSection[] sectionCache;
++
++ // the exact same as above, except for storing fast access to SWMRNibbleArray
++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
++ // index = x + (z * 5) + (y * 25)
++ protected final SWMRNibbleArray[] nibbleCache;
++
++ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
++ // index = x + (z * 5) + (y * 25)
++ protected final boolean[] notifyUpdateCache;
++
++ // always initialsed during start of lighting.
++ // index = x + (z * 5)
++ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
++
++ // index = x + (z * 5)
++ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
++
++ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
++ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
++ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
++
++ protected int encodeOffsetX;
++ protected int encodeOffsetY;
++ protected int encodeOffsetZ;
++
++ protected int coordinateOffset;
++
++ protected int chunkOffsetX;
++ protected int chunkOffsetY;
++ protected int chunkOffsetZ;
++
++ protected int chunkIndexOffset;
++ protected int chunkSectionIndexOffset;
++
++ protected final boolean skylightPropagator;
++ protected final int emittedLightMask;
++ protected final boolean isClientSide;
++
++ protected final Level world;
++ protected final int minLightSection;
++ protected final int maxLightSection;
++ protected final int minSection;
++ protected final int maxSection;
++
++ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
++ this.skylightPropagator = skylightPropagator;
++ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
++ this.isClientSide = world.isClientSide;
++ this.world = world;
++ this.minLightSection = WorldUtil.getMinLightSection(world);
++ this.maxLightSection = WorldUtil.getMaxLightSection(world);
++ this.minSection = WorldUtil.getMinSection(world);
++ this.maxSection = WorldUtil.getMaxSection(world);
++
++ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
++ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
++ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
++ }
++
++ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
++ // 31 = center + encodeOffset
++ this.encodeOffsetX = 31 - centerX;
++ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
++ this.encodeOffsetZ = 31 - centerZ;
++
++ // coordinateIndex = x | (z << 6) | (y << 12)
++ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
++
++ // 2 = (centerX >> 4) + chunkOffset
++ this.chunkOffsetX = 2 - (centerX >> 4);
++ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
++ this.chunkOffsetZ = 2 - (centerZ >> 4);
++
++ // chunk index = x + (5 * z)
++ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
++
++ // chunk section index = x + (5 * z) + ((5*5) * y)
++ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
++ }
++
++ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
++ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
++ final int centerChunkX = centerX >> 4;
++ final int centerChunkY = centerY >> 4;
++ final int centerChunkZ = centerZ >> 4;
++
++ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
++
++ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
++
++ for (int dz = -radius; dz <= radius; ++dz) {
++ for (int dx = -radius; dx <= radius; ++dx) {
++ final int cx = centerChunkX + dx;
++ final int cz = centerChunkZ + dz;
++ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
++ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
++
++ if (chunk == null) {
++ if (relaxed | isTwoRadius) {
++ continue;
++ }
++ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
++ }
++
++ if (!this.canUseChunk(chunk)) {
++ continue;
++ }
++
++ this.setChunkInCache(cx, cz, chunk);
++ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
++ if (!isTwoRadius) {
++ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
++ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
+ }
+ }
++ }
++ }
+
-+ return -1;
++ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
++ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
++ }
++
++ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
++ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
++ }
++
++ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
++ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
++ }
++
++ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
++ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
++ }
++
++ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
++ this.setChunkSectionInCache(chunkX, cy, chunkZ,
++ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
+ }
++ }
+
-+ public boolean remove(final E entity) {
-+ final int idx = this.indexOf(entity);
-+ if (idx == -1) {
-+ return false;
++ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
++ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
++ }
++
++ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
++ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
++
++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
++ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
++ }
++
++ return ret;
++ }
++
++ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
++ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
++ }
++
++ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
++ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
++ }
++ }
++
++ protected final void updateVisible(final LightChunkGetter lightAccess) {
++ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
++ final SWMRNibbleArray nibble = this.nibbleCache[index];
++ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
++ continue;
+ }
+
-+ final int size = --this.size;
-+ final E[] storage = this.storage;
-+ if (idx != size) {
-+ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
++ final int chunkX = (index % 5) - this.chunkOffsetX;
++ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
++ final int ySections = this.maxSection - this.minSection + 1;
++ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
++ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
++ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
+ }
++ }
++ }
+
-+ storage[size] = null;
++ protected final void destroyCaches() {
++ Arrays.fill(this.sectionCache, null);
++ Arrays.fill(this.nibbleCache, null);
++ Arrays.fill(this.chunkCache, null);
++ Arrays.fill(this.emptinessMapCache, null);
++ if (this.isClientSide) {
++ Arrays.fill(this.notifyUpdateCache, false);
++ }
++ }
+
-+ return true;
++ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
++ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
++
++ if (section != null) {
++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
+ }
+
-+ public boolean has(final E entity) {
-+ return this.indexOf(entity) != -1;
++ return AIR_BLOCK_STATE;
++ }
++
++ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
++ final LevelChunkSection section = this.sectionCache[sectionIndex];
++
++ if (section != null) {
++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
+ }
++
++ return AIR_BLOCK_STATE;
+ }
+
-+ protected static final class EntityCollectionBySection {
++ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
++ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
-+ protected final ChunkEntitySlices manager;
-+ protected final long[] nonEmptyBitset;
-+ protected final BasicEntityList<Entity>[] entitiesBySection;
-+ protected int count;
++ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
++ }
+
-+ public EntityCollectionBySection(final ChunkEntitySlices manager) {
-+ this.manager = manager;
++ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
-+ final int sectionCount = manager.maxSection - manager.minSection + 1;
++ return nibble == null ? 0 : nibble.getUpdating(localIndex);
++ }
+
-+ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE
-+ this.entitiesBySection = new BasicEntityList[sectionCount];
++ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
++ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
++
++ if (nibble != null) {
++ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
++ if (this.isClientSide) {
++ int cx1 = (worldX - 1) >> 4;
++ int cx2 = (worldX + 1) >> 4;
++ int cy1 = (worldY - 1) >> 4;
++ int cy2 = (worldY + 1) >> 4;
++ int cz1 = (worldZ - 1) >> 4;
++ int cz2 = (worldZ + 1) >> 4;
++ for (int x = cx1; x <= cx2; ++x) {
++ for (int y = cy1; y <= cy2; ++y) {
++ for (int z = cz1; z <= cz2; ++z) {
++ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
++ }
++ }
++ }
++ }
+ }
++ }
+
-+ public void addEntity(final Entity entity, final int sectionIndex) {
-+ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
++ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
++ if (this.isClientSide) {
++ int cx1 = (worldX - 1) >> 4;
++ int cx2 = (worldX + 1) >> 4;
++ int cy1 = (worldY - 1) >> 4;
++ int cy2 = (worldY + 1) >> 4;
++ int cz1 = (worldZ - 1) >> 4;
++ int cz2 = (worldZ + 1) >> 4;
++ for (int x = cx1; x <= cx2; ++x) {
++ for (int y = cy1; y <= cy2; ++y) {
++ for (int z = cz1; z <= cz2; ++z) {
++ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
++ }
++ }
++ }
++ }
++ }
+
-+ if (list != null && list.has(entity)) {
++ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
++
++ if (nibble != null) {
++ nibble.set(localIndex, level);
++ if (this.isClientSide) {
++ int cx1 = (worldX - 1) >> 4;
++ int cx2 = (worldX + 1) >> 4;
++ int cy1 = (worldY - 1) >> 4;
++ int cy2 = (worldY + 1) >> 4;
++ int cz1 = (worldZ - 1) >> 4;
++ int cz2 = (worldZ + 1) >> 4;
++ for (int x = cx1; x <= cx2; ++x) {
++ for (int y = cy1; y <= cy2; ++y) {
++ for (int z = cz1; z <= cz2; ++z) {
++ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
++ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
++ }
++
++ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
++ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
++ }
++
++ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
++ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
++ }
++
++ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
++ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
++
++ for (int i = 0, len = ret.length; i < len; ++i) {
++ ret[i] = new SWMRNibbleArray(null, true);
++ }
++
++ return ret;
++ }
++
++ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
++
++ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
++
++ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
++
++ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
++
++ protected abstract boolean canUseChunk(final ChunkAccess chunk);
++
++ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
++ final Set<BlockPos> positions, final Boolean[] changedSections) {
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
++ try {
++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
++ if (chunk == null) {
+ return;
+ }
++ if (changedSections != null) {
++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
++ if (ret != null) {
++ this.setEmptinessMap(chunk, ret);
++ }
++ }
++ if (!positions.isEmpty()) {
++ this.propagateBlockChanges(lightAccess, chunk, positions);
++ }
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
+
-+ if (list == null) {
-+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
-+ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1)));
++ // subclasses should not initialise caches, as this will always be done by the super call
++ // subclasses should not invoke updateVisible, as this will always be done by the super call
++ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
++
++ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
++
++ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
++ // if ret == expect, then expect is the correct light value for pos
++ // if ret < expect, then ret is the real light value
++ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
++ final int expect);
++
++ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
++ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
++
++ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
++ final int chunkX, final int chunkY, final int chunkZ) {
++ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
++ if (currNibble == null) {
++ return;
++ }
++
++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
++ final int neighbourOffX = direction.x;
++ final int neighbourOffZ = direction.z;
++
++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
++ chunkY, chunkZ + neighbourOffZ);
++
++ if (neighbourNibble == null) {
++ continue;
+ }
+
-+ list.add(entity);
-+ ++this.count;
++ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
++ // both are zero, nothing to check.
++ continue;
++ }
++
++ // this chunk
++ final int incX;
++ final int incZ;
++ final int startX;
++ final int startZ;
++
++ if (neighbourOffX != 0) {
++ // x direction
++ incX = 0;
++ incZ = 1;
++
++ if (direction.x < 0) {
++ // negative
++ startX = chunkX << 4;
++ } else {
++ startX = chunkX << 4 | 15;
++ }
++ startZ = chunkZ << 4;
++ } else {
++ // z direction
++ incX = 1;
++ incZ = 0;
++
++ if (neighbourOffZ < 0) {
++ // negative
++ startZ = chunkZ << 4;
++ } else {
++ startZ = chunkZ << 4 | 15;
++ }
++ startX = chunkX << 4;
++ }
++
++ int centerDelayedChecks = 0;
++ int neighbourDelayedChecks = 0;
++ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
++ final int neighbourX = currX + neighbourOffX;
++ final int neighbourZ = currZ + neighbourOffZ;
++
++ final int currentIndex = (currX & 15) |
++ ((currZ & 15)) << 4 |
++ ((currY & 15) << 8);
++ final int currentLevel = currNibble.getUpdating(currentIndex);
++
++ final int neighbourIndex =
++ (neighbourX & 15) |
++ ((neighbourZ & 15)) << 4 |
++ ((currY & 15) << 8);
++ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
++
++ // the checks are delayed because the checkBlock method clobbers light values - which then
++ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
++ // way, they do have a negative performance impact due to simply queueing more values
++
++ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
++ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
++ }
++
++ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
++ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
++ }
++ }
++ }
++
++ final int currentChunkOffX = chunkX << 4;
++ final int currentChunkOffZ = chunkZ << 4;
++ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
++ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
++ final int chunkOffY = chunkY << 4;
++ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
++ // try to queue neighbouring data together
++ // index = x | (z << 4) | (y << 8)
++ if (i < centerDelayedChecks) {
++ final int value = this.chunkCheckDelayedUpdatesCenter[i];
++ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
++ chunkOffY | (value >>> 8),
++ currentChunkOffZ | ((value >>> 4) & 0xF));
++ }
++ if (i < neighbourDelayedChecks) {
++ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
++ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
++ chunkOffY | (value >>> 8),
++ neighbourChunkOffZ | ((value >>> 4) & 0xF));
++ }
++ }
+ }
++ }
+
-+ public void removeEntity(final Entity entity, final int sectionIndex) {
-+ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
++ final ChunkPos chunkPos = chunk.getPos();
++ final int chunkX = chunkPos.x;
++ final int chunkZ = chunkPos.z;
+
-+ if (list == null || !list.remove(entity)) {
-+ return;
++ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
++ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
++ }
++
++ this.performLightDecrease(lightAccess);
++ }
++
++ // subclasses should not initialise caches, as this will always be done by the super call
++ // subclasses should not invoke updateVisible, as this will always be done by the super call
++ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
++ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
++ // This does not resolve skylight source problems.
++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
++ final ChunkPos chunkPos = chunk.getPos();
++ final int chunkX = chunkPos.x;
++ final int chunkZ = chunkPos.z;
++
++ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
++ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
++ }
++
++ this.performLightDecrease(lightAccess);
++ }
++
++ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
++ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
++ final ChunkPos chunkPos = chunk.getPos();
++ final int chunkX = chunkPos.x;
++ final int chunkZ = chunkPos.z;
++
++ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
++ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
++ if (currNibble == null) {
++ continue;
+ }
++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
++ final int neighbourOffX = direction.x;
++ final int neighbourOffZ = direction.z;
+
-+ --this.count;
++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
++ currSectionY, chunkZ + neighbourOffZ);
+
-+ if (list.isEmpty()) {
-+ this.entitiesBySection[sectionIndex] = null;
-+ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1)));
++ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
++ // can't pull from 0
++ continue;
++ }
++
++ // neighbour chunk
++ final int incX;
++ final int incZ;
++ final int startX;
++ final int startZ;
++
++ if (neighbourOffX != 0) {
++ // x direction
++ incX = 0;
++ incZ = 1;
++
++ if (direction.x < 0) {
++ // negative
++ startX = (chunkX << 4) - 1;
++ } else {
++ startX = (chunkX << 4) + 16;
++ }
++ startZ = chunkZ << 4;
++ } else {
++ // z direction
++ incX = 1;
++ incZ = 0;
++
++ if (neighbourOffZ < 0) {
++ // negative
++ startZ = (chunkZ << 4) - 1;
++ } else {
++ startZ = (chunkZ << 4) + 16;
++ }
++ startX = chunkX << 4;
++ }
++
++ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
++ final int encodeOffset = this.coordinateOffset;
++
++ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
++ final int level = neighbourNibble.getUpdating(
++ (currX & 15)
++ | ((currZ & 15) << 4)
++ | ((currY & 15) << 8)
++ );
++
++ if (level <= 1) {
++ // nothing to propagate
++ continue;
++ }
++
++ this.appendToIncreaseQueue(
++ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((level & 0xFL) << (6 + 6 + 16))
++ | (propagateDirection << (6 + 6 + 16 + 4))
++ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
++ );
++ }
++ }
+ }
+ }
++ }
+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ if (this.count == 0) {
++ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
++ final LevelChunkSection[] sections = chunk.getSections();
++ final Boolean[] ret = new Boolean[sections.length];
++
++ for (int i = 0; i < sections.length; ++i) {
++ if (sections[i] == null || sections[i].hasOnlyAir()) {
++ ret[i] = Boolean.TRUE;
++ } else {
++ ret[i] = Boolean.FALSE;
++ }
++ }
++
++ return ret;
++ }
++
++ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
++ try {
++ // force current chunk into cache
++ this.setChunkInCache(chunkX, chunkZ, chunk);
++ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
++ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
++ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
++
++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
++ if (ret != null) {
++ this.setEmptinessMap(chunk, ret);
++ }
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
++
++ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
++ final Boolean[] emptinessChanges) {
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
++ try {
++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
++ if (chunk == null) {
+ return;
+ }
++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
++ if (ret != null) {
++ this.setEmptinessMap(chunk, ret);
++ }
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
+
-+ final int minSection = this.manager.minSection;
-+ final int maxSection = this.manager.maxSection;
++ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++ // subclasses should not initialise caches, as this will always be done by the super call
++ // subclasses should not invoke updateVisible, as this will always be done by the super call
++ // subclasses are guaranteed that this is always called before a changed block set
++ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
++ // rets non-null when the emptiness map changed and needs to be updated
++ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
++ final Boolean[] emptinessChanges, final boolean unlit) {
++ final Level world = (Level)lightAccess.getLevel();
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
++ boolean[] ret = null;
++ final boolean needsInit = unlit || chunkEmptinessMap == null;
++ if (needsInit) {
++ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
++ }
+
-+ if (list == null) {
++ // update emptiness map
++ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
++ Boolean valueBoxed = emptinessChanges[sectionIndex];
++ if (valueBoxed == null) {
++ if (!needsInit) {
+ continue;
+ }
++ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
++ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
++ }
++ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
++ }
+
-+ final Entity[] storage = list.storage;
++ // now init neighbour nibbles
++ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
++ final Boolean valueBoxed = emptinessChanges[sectionIndex];
++ final int sectionY = sectionIndex + this.minSection;
++ if (valueBoxed == null) {
++ continue;
++ }
+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
++ final boolean empty = valueBoxed.booleanValue();
+
-+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
-+ continue;
++ if (empty) {
++ continue;
++ }
++
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ // if we're not empty, we also need to initialise nibbles
++ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
++ final boolean extrude = (dx | dz) != 0 || !unlit;
++ for (int dy = 1; dy >= -1; --dy) {
++ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
+ }
++ }
++ }
++ }
+
-+ if (predicate != null && !predicate.test(entity)) {
-+ continue;
++ // check for de-init and lazy-init
++ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
++ // init checks.
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ // does this neighbour have 1 radius loaded?
++ boolean neighboursLoaded = true;
++ neighbour_loaded_search:
++ for (int dz2 = -1; dz2 <= 1; ++dz2) {
++ for (int dx2 = -1; dx2 <= 1; ++dx2) {
++ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
++ neighboursLoaded = false;
++ break neighbour_loaded_search;
++ }
+ }
++ }
+
-+ into.add(entity);
++ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
++ // check neighbours to see if we need to de-init this one
++ boolean allEmpty = true;
++ neighbour_search:
++ for (int dy2 = -1; dy2 <= 1; ++dy2) {
++ for (int dz2 = -1; dz2 <= 1; ++dz2) {
++ for (int dx2 = -1; dx2 <= 1; ++dx2) {
++ final int y = sectionY + dy2;
++ if (y < this.minSection || y > this.maxSection) {
++ // empty
++ continue;
++ }
++ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
++ if (emptinessMap != null) {
++ if (!emptinessMap[y - this.minSection]) {
++ allEmpty = false;
++ break neighbour_search;
++ }
++ } else {
++ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
++ if (section != null && !section.hasOnlyAir()) {
++ allEmpty = false;
++ break neighbour_search;
++ }
++ }
++ }
++ }
++ }
++
++ if (allEmpty & neighboursLoaded) {
++ // can only de-init when neighbours are loaded
++ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
++ // to be correct
++
++ // all were empty, so de-init
++ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
++ } else if (!allEmpty) {
++ // must init
++ final boolean extrude = (dx | dz) != 0 || !unlit;
++ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
++ }
+ }
+ }
+ }
+
-+ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List<Entity> into,
-+ final Predicate<? super Entity> predicate) {
-+ if (this.count == 0) {
++ return ret;
++ }
++
++ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
++ try {
++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
++ if (chunk == null) {
+ return;
+ }
++ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
+
-+ final int minSection = this.manager.minSection;
-+ final int maxSection = this.manager.maxSection;
++ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
++ try {
++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
++ if (chunk == null) {
++ return;
++ }
++ this.checkChunkEdges(lightAccess, chunk, sections);
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++ // subclasses should not initialise caches, as this will always be done by the super call
++ // subclasses should not invoke updateVisible, as this will always be done by the super call
++ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
++ // chunks light values with respect to neighbours
++ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
++ // does not need to detect empty chunks itself (and it should do no handling for them either!)
++ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++ try {
++ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
++ // force current chunk into cache
++ this.setChunkInCache(chunkX, chunkZ, chunk);
++ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
++ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
++ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
++
++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
++ if (ret != null) {
++ this.setEmptinessMap(chunk, ret);
++ }
++ this.lightChunk(lightAccess, chunk, true);
++ this.setNibbles(chunk, nibbles);
++ this.updateVisible(lightAccess);
++ } finally {
++ this.destroyCaches();
++ }
++ }
++
++ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
++ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
++ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
++ // the region of chunks to relight
++ // it's required that tickets are added for each chunk to keep them loaded
++ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
++ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
++
++ final int[] neighbourLightOrder = new int[] {
++ // d = 0
++ 0, 0,
++ // d = 1
++ -1, 0,
++ 0, -1,
++ 1, 0,
++ 0, 1,
++ // d = 2
++ -1, 1,
++ 1, 1,
++ -1, -1,
++ 1, -1,
++ };
+
-+ if (list == null) {
++ int lightCalls = 0;
++
++ for (final ChunkPos chunkPos : chunks) {
++ final int chunkX = chunkPos.x;
++ final int chunkZ = chunkPos.z;
++ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
++ if (chunk == null || !this.canUseChunk(chunk)) {
++ throw new IllegalStateException();
++ }
++
++ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
++ final int dx = neighbourLightOrder[i];
++ final int dz = neighbourLightOrder[i + 1];
++ final int neighbourX = dx + chunkX;
++ final int neighbourZ = dz + chunkZ;
++
++ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
++ if (neighbour == null || !this.canUseChunk(neighbour)) {
+ continue;
+ }
+
-+ final Entity[] storage = list.storage;
++ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
++ // lit already called for neighbour, no need to light it now
++ continue;
++ }
+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
++ // light neighbour chunk
++ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
++ try {
++ // insert all neighbouring chunks for this neighbour that we have data for
++ for (int dz2 = -1; dz2 <= 1; ++dz2) {
++ for (int dx2 = -1; dx2 <= 1; ++dx2) {
++ final int neighbourX2 = neighbourX + dx2;
++ final int neighbourZ2 = neighbourZ + dz2;
++ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
++ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
++ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
++ continue;
++ }
+
-+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
++ if (nibbles == null) {
++ // we haven't lit this chunk
++ continue;
++ }
++
++ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
++ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
++ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
++ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
++ }
++ }
++
++ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
++
++ // now insert the neighbour chunk and light it
++ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
++ nibblesByChunk.put(key, nibbles);
++
++ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
++ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
++ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
++
++ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
++ emptinessMapByChunk.put(key, neighbourEmptiness);
++ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
++ this.setEmptinessMap(neighbour, neighbourEmptiness);
++ }
++
++ this.lightChunk(lightAccess, neighbour, false);
++ } finally {
++ this.destroyCaches();
++ }
++ }
++
++ // done lighting all neighbours, so the chunk is now fully lit
++
++ // make sure nibbles are fully updated before calling back
++ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ for (final SWMRNibbleArray nibble : nibbles) {
++ nibble.updateVisible();
++ }
++
++ this.setNibbles(chunk, nibbles);
++
++ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
++ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ));
++ }
++
++ // now do callback
++ if (chunkLightCallback != null) {
++ chunkLightCallback.accept(chunkPos);
++ }
++ ++lightCalls;
++ }
++
++ if (onComplete != null) {
++ onComplete.accept(lightCalls);
++ }
++ }
++
++ // contains:
++ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
++ // next 4 bits: propagated light level (0, 15]
++ // next 6 bits: propagation direction bitset
++ // next 24 bits: unused
++ // last 3 bits: state flags
++ // state flags:
++ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
++ // updates for block sources
++ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
++ // whether the propagation needs to check if its current level is equal to the expected level
++ // used only in increase propagation
++ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
++ // whether the propagation needs to consider if its block is conditionally transparent
++ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
++
++ protected long[] increaseQueue = new long[16 * 16 * 16];
++ protected int increaseQueueInitialLength;
++ protected long[] decreaseQueue = new long[16 * 16 * 16];
++ protected int decreaseQueueInitialLength;
++
++ protected final long[] resizeIncreaseQueue() {
++ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
++ }
++
++ protected final long[] resizeDecreaseQueue() {
++ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
++ }
++
++ protected final void appendToIncreaseQueue(final long value) {
++ final int idx = this.increaseQueueInitialLength++;
++ long[] queue = this.increaseQueue;
++ if (idx >= queue.length) {
++ queue = this.resizeIncreaseQueue();
++ queue[idx] = value;
++ } else {
++ queue[idx] = value;
++ }
++ }
++
++ protected final void appendToDecreaseQueue(final long value) {
++ final int idx = this.decreaseQueueInitialLength++;
++ long[] queue = this.decreaseQueue;
++ if (idx >= queue.length) {
++ queue = this.resizeDecreaseQueue();
++ queue[idx] = value;
++ } else {
++ queue[idx] = value;
++ }
++ }
++
++ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
++ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
++ static {
++ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
++ final List<AxisDirection> directions = new ArrayList<>();
++ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
++ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
++ }
++ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
++ }
++ }
++
++ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
++ final BlockGetter world = lightAccess.getLevel();
++ long[] queue = this.increaseQueue;
++ int queueReadIndex = 0;
++ int queueLength = this.increaseQueueInitialLength;
++ this.increaseQueueInitialLength = 0;
++ final int decodeOffsetX = -this.encodeOffsetX;
++ final int decodeOffsetY = -this.encodeOffsetY;
++ final int decodeOffsetZ = -this.encodeOffsetZ;
++ final int encodeOffset = this.coordinateOffset;
++ final int sectionOffset = this.chunkSectionIndexOffset;
++
++ while (queueReadIndex < queueLength) {
++ final long queueValue = queue[queueReadIndex++];
++
++ final int posX = ((int)queueValue & 63) + decodeOffsetX;
++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
++ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
++
++ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
++ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
++ // not at the level we expect, so something changed.
++ continue;
++ }
++ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
++ // these are used to restore block sources after a propagation decrease
++ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
++ }
++
++ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
++ // we don't need to worry about our state here.
++ for (final AxisDirection propagate : checkDirections) {
++ final int offX = posX + propagate.x;
++ final int offY = posY + propagate.y;
++ final int offZ = posZ + propagate.z;
++
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
++
++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
++ final int currentLevel;
++ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
++ continue; // already at the level we want or unloaded
++ }
++
++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
++ if (blockState == null) {
+ continue;
+ }
++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
++ if (opacityCached != -1) {
++ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
++ if (targetLevel > currentLevel) {
++ currentNibble.set(localIndex, targetLevel);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
++ continue;
++ }
++ }
++ continue;
++ } else {
++ this.mutablePos1.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
-+ if (predicate == null || predicate.test(entity)) {
-+ into.add(entity);
-+ } // else: continue to test the ender dragon parts
++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
++ continue;
++ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ if (entity instanceof EnderDragon) {
-+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) {
-+ if (part == except || !part.getBoundingBox().intersects(box)) {
++ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
++ if (targetLevel <= currentLevel) {
++ continue;
++ }
++
++ currentNibble.set(localIndex, targetLevel);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
++ | (flags);
++ }
++ continue;
++ }
++ }
++ } else {
++ // we actually need to worry about our state here
++ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
++ this.mutablePos2.set(posX, posY, posZ);
++ for (final AxisDirection propagate : checkDirections) {
++ final int offX = posX + propagate.x;
++ final int offY = posY + propagate.y;
++ final int offZ = posZ + propagate.z;
++
++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
++
++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
++ continue;
++ }
++
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
++
++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
++ final int currentLevel;
++
++ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
++ continue; // already at the level we want
++ }
++
++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
++ if (blockState == null) {
++ continue;
++ }
++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
++ if (opacityCached != -1) {
++ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
++ if (targetLevel > currentLevel) {
++ currentNibble.set(localIndex, targetLevel);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
+ continue;
+ }
++ }
++ continue;
++ } else {
++ this.mutablePos1.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
-+ if (predicate != null && !predicate.test(part)) {
++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ continue;
+ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ into.add(part);
++ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
++ if (targetLevel <= currentLevel) {
++ continue;
++ }
++
++ currentNibble.set(localIndex, targetLevel);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
++ | (flags);
+ }
++ continue;
+ }
+ }
+ }
+ }
++ }
+
-+ public void getEntitiesWithEnderDragonParts(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
-+ final Predicate<? super Entity> predicate) {
-+ if (this.count == 0) {
-+ return;
-+ }
++ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
++ final BlockGetter world = lightAccess.getLevel();
++ long[] queue = this.decreaseQueue;
++ long[] increaseQueue = this.increaseQueue;
++ int queueReadIndex = 0;
++ int queueLength = this.decreaseQueueInitialLength;
++ this.decreaseQueueInitialLength = 0;
++ int increaseQueueLength = this.increaseQueueInitialLength;
++ final int decodeOffsetX = -this.encodeOffsetX;
++ final int decodeOffsetY = -this.encodeOffsetY;
++ final int decodeOffsetZ = -this.encodeOffsetZ;
++ final int encodeOffset = this.coordinateOffset;
++ final int sectionOffset = this.chunkSectionIndexOffset;
++ final int emittedMask = this.emittedLightMask;
+
-+ final int minSection = this.manager.minSection;
-+ final int maxSection = this.manager.maxSection;
++ while (queueReadIndex < queueLength) {
++ final long queueValue = queue[queueReadIndex++];
+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++ final int posX = ((int)queueValue & 63) + decodeOffsetX;
++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
++ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
++ // we don't need to worry about our state here.
++ for (final AxisDirection propagate : checkDirections) {
++ final int offX = posX + propagate.x;
++ final int offY = posY + propagate.y;
++ final int offZ = posZ + propagate.z;
+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
-+ if (list == null) {
-+ continue;
++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
++ final int lightLevel;
++
++ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
++ // already at lowest (or unloaded), nothing we can do
++ continue;
++ }
++
++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
++ if (blockState == null) {
++ continue;
++ }
++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
++ if (opacityCached != -1) {
++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
++ if (lightLevel > targetLevel) {
++ // it looks like another source propagated here, so re-propagate it
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((lightLevel & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | FLAG_RECHECK_LEVEL;
++ continue;
++ }
++ final int emittedLight = blockState.getLightEmission() & emittedMask;
++ if (emittedLight != 0) {
++ // re-propagate source
++ // note: do not set recheck level, or else the propagation will fail
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((emittedLight & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
++ }
++
++ currentNibble.set(localIndex, 0);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
++ continue;
++ }
++ continue;
++ } else {
++ this.mutablePos1.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
++
++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
++ continue;
++ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
++
++ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
++ if (lightLevel > targetLevel) {
++ // it looks like another source propagated here, so re-propagate it
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((lightLevel & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (FLAG_RECHECK_LEVEL | flags);
++ continue;
++ }
++ final int emittedLight = blockState.getLightEmission() & emittedMask;
++ if (emittedLight != 0) {
++ // re-propagate source
++ // note: do not set recheck level, or else the propagation will fail
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((emittedLight & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (flags | FLAG_WRITE_LEVEL);
++ }
++
++ currentNibble.set(localIndex, 0);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 0) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
++ | flags;
++ }
++ continue;
++ }
+ }
++ } else {
++ // we actually need to worry about our state here
++ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
++ this.mutablePos2.set(posX, posY, posZ);
++ for (final AxisDirection propagate : checkDirections) {
++ final int offX = posX + propagate.x;
++ final int offY = posY + propagate.y;
++ final int offZ = posZ + propagate.z;
+
-+ final Entity[] storage = list.storage;
++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
+
-+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
+ }
+
-+ if (predicate == null || predicate.test(entity)) {
-+ into.add(entity);
-+ } // else: continue to test the ender dragon parts
++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
++ final int lightLevel;
+
-+ if (entity instanceof EnderDragon) {
-+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) {
-+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
-+ continue;
++ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
++ // already at lowest (or unloaded), nothing we can do
++ continue;
++ }
++
++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
++ if (blockState == null) {
++ continue;
++ }
++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
++ if (opacityCached != -1) {
++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
++ if (lightLevel > targetLevel) {
++ // it looks like another source propagated here, so re-propagate it
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
+ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((lightLevel & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | FLAG_RECHECK_LEVEL;
++ continue;
++ }
++ final int emittedLight = blockState.getLightEmission() & emittedMask;
++ if (emittedLight != 0) {
++ // re-propagate source
++ // note: do not set recheck level, or else the propagation will fail
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((emittedLight & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
++ }
+
-+ if (predicate != null && !predicate.test(part)) {
++ currentNibble.set(localIndex, 0);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
++ continue;
++ }
++ continue;
++ } else {
++ this.mutablePos1.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
++
++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ continue;
+ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ into.add(part);
++ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
++ if (lightLevel > targetLevel) {
++ // it looks like another source propagated here, so re-propagate it
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((lightLevel & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (FLAG_RECHECK_LEVEL | flags);
++ continue;
++ }
++ final int emittedLight = blockState.getLightEmission() & emittedMask;
++ if (emittedLight != 0) {
++ // re-propagate source
++ // note: do not set recheck level, or else the propagation will fail
++ if (increaseQueueLength >= increaseQueue.length) {
++ increaseQueue = this.resizeIncreaseQueue();
++ }
++ increaseQueue[increaseQueueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((emittedLight & 0xFL) << (6 + 6 + 16))
++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
++ | (flags | FLAG_WRITE_LEVEL);
++ }
++
++ currentNibble.set(localIndex, 0);
++ this.postLightUpdate(offX, offY, offZ);
++
++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
++ }
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
++ | flags;
+ }
++ continue;
+ }
+ }
+ }
+ }
+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ if (this.count == 0) {
-+ return;
++ // propagate sources we clobbered
++ this.increaseQueueInitialLength = increaseQueueLength;
++ this.performLightIncrease(lightAccess);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82f5938862
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
+@@ -0,0 +1,930 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.shorts.ShortCollection;
++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.SectionPos;
++import net.minecraft.server.level.ChunkLevel;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.DataLayer;
++import net.minecraft.world.level.chunk.LightChunkGetter;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.lighting.LayerLightEventListener;
++import net.minecraft.world.level.lighting.LevelLightEngine;
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Set;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.function.BooleanSupplier;
++import java.util.function.Consumer;
++import java.util.function.IntConsumer;
++
++public final class StarLightInterface {
++
++ public static final TicketType<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo);
++ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT);
++ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input
++ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL;
++
++ /**
++ * Can be {@code null}, indicating the light is all empty.
++ */
++ public final Level world;
++ public final LightChunkGetter lightAccess;
++
++ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
++ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
++
++ private final LightQueue lightQueue;
++
++ private final LayerLightEventListener skyReader;
++ private final LayerLightEventListener blockReader;
++ private final boolean isClientSide;
++
++ public final int minSection;
++ public final int maxSection;
++ public final int minLightSection;
++ public final int maxLightSection;
++
++ public final LevelLightEngine lightEngine;
++
++ private final boolean hasBlockLight;
++ private final boolean hasSkyLight;
++
++ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
++ this.lightAccess = lightAccess;
++ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
++ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
++ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
++ this.isClientSide = !(this.world instanceof ServerLevel);
++ if (this.world == null) {
++ this.minSection = -4;
++ this.maxSection = 19;
++ this.minLightSection = -5;
++ this.maxLightSection = 20;
++ } else {
++ this.minSection = WorldUtil.getMinSection(this.world);
++ this.maxSection = WorldUtil.getMaxSection(this.world);
++ this.minLightSection = WorldUtil.getMinLightSection(this.world);
++ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
++ }
++
++ if (this.world instanceof ServerLevel) {
++ this.lightQueue = new ServerLightQueue(this);
++ } else {
++ this.lightQueue = new ClientLightQueue(this);
++ }
++
++ this.lightEngine = lightEngine;
++ this.hasBlockLight = hasBlockLight;
++ this.hasSkyLight = hasSkyLight;
++ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
++ @Override
++ public void checkBlock(final BlockPos blockPos) {
++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
+ }
+
-+ final int minSection = this.manager.minSection;
-+ final int maxSection = this.manager.maxSection;
++ @Override
++ public void propagateLightSources(final ChunkPos chunkPos) {
++ throw new UnsupportedOperationException();
++ }
+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
++ @Override
++ public boolean hasLightWork() {
++ // not really correct...
++ return StarLightInterface.this.hasUpdates();
++ }
+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
++ @Override
++ public int runLightUpdates() {
++ throw new UnsupportedOperationException();
++ }
+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
++ @Override
++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
++ throw new UnsupportedOperationException();
++ }
+
-+ if (list == null) {
-+ continue;
++ @Override
++ public DataLayer getDataLayerData(final SectionPos pos) {
++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
++ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
++ return null;
+ }
+
-+ final Entity[] storage = list.storage;
++ final int sectionY = pos.getY();
+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
++ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
++ return null;
++ }
+
-+ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) {
-+ continue;
++ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) {
++ return null;
++ }
++
++ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
++ }
++
++ @Override
++ public int getLightValue(final BlockPos blockPos) {
++ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
++ }
++
++ @Override
++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
++ StarLightInterface.this.sectionChange(pos, notReady);
++ }
++ };
++ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
++ @Override
++ public void checkBlock(final BlockPos blockPos) {
++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
++ }
++
++ @Override
++ public void propagateLightSources(final ChunkPos chunkPos) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean hasLightWork() {
++ // not really correct...
++ return StarLightInterface.this.hasUpdates();
++ }
++
++ @Override
++ public int runLightUpdates() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public DataLayer getDataLayerData(final SectionPos pos) {
++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
++
++ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
++ return null;
++ }
++
++ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
++ }
++
++ @Override
++ public int getLightValue(final BlockPos blockPos) {
++ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
++ }
++
++ @Override
++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
++ StarLightInterface.this.sectionChange(pos, notReady);
++ }
++ };
++ }
++
++ public ClientLightQueue getClientLightQueue() {
++ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) {
++ return clientLightQueue;
++ }
++ return null;
++ }
++
++ public ServerLightQueue getServerLightQueue() {
++ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) {
++ return serverLightQueue;
++ }
++ return null;
++ }
++
++ public boolean hasSkyLight() {
++ return this.hasSkyLight;
++ }
++
++ public boolean hasBlockLight() {
++ return this.hasBlockLight;
++ }
++
++ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
++ if (!this.hasSkyLight) {
++ return 0;
++ }
++ final int x = blockPos.getX();
++ int y = blockPos.getY();
++ final int z = blockPos.getZ();
++
++ final int minSection = this.minSection;
++ final int maxSection = this.maxSection;
++ final int minLightSection = this.minLightSection;
++ final int maxLightSection = this.maxLightSection;
++
++ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
++ return 15;
++ }
++
++ int sectionY = y >> 4;
++
++ if (sectionY > maxLightSection) {
++ return 15;
++ }
++
++ if (sectionY < minLightSection) {
++ sectionY = minLightSection;
++ y = sectionY << 4;
++ }
++
++ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
++ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
++
++ if (!immediate.isNullNibbleVisible()) {
++ return immediate.getVisible(x, y, z);
++ }
++
++ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
++
++ if (emptinessMap == null) {
++ return 15;
++ }
++
++ // are we above this chunk's lowest empty section?
++ int lowestY = minLightSection - 1;
++ for (int currY = maxSection; currY >= minSection; --currY) {
++ if (emptinessMap[currY - minSection]) {
++ continue;
++ }
++
++ // should always be full lit here
++ lowestY = currY;
++ break;
++ }
++
++ if (sectionY > lowestY) {
++ return 15;
++ }
++
++ // this nibble is going to depend solely on the skylight data above it
++ // find first non-null data above (there does exist one, as we just found it above)
++ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
++ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
++ if (!nibble.isNullNibbleVisible()) {
++ return nibble.getVisible(x, 0, z);
++ }
++ }
++
++ // should never reach here
++ return 15;
++ }
++
++ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
++ if (!this.hasBlockLight) {
++ return 0;
++ }
++ final int y = blockPos.getY();
++ final int cy = y >> 4;
++
++ final int minLightSection = this.minLightSection;
++ final int maxLightSection = this.maxLightSection;
++
++ if (cy < minLightSection || cy > maxLightSection) {
++ return 0;
++ }
++
++ if (chunk == null) {
++ return 0;
++ }
++
++ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection];
++ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
++ }
++
++ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
++ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
++
++ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
++ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
++ if (sky == 15) {
++ return 15;
++ }
++ final int block = this.getBlockLightValue(pos, chunk);
++ return Math.max(sky, block);
++ }
++
++ public LayerLightEventListener getSkyReader() {
++ return this.skyReader;
++ }
++
++ public LayerLightEventListener getBlockReader() {
++ return this.blockReader;
++ }
++
++ public boolean isClientSide() {
++ return this.isClientSide;
++ }
++
++ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
++ if (this.world == null) {
++ // empty world
++ return null;
++ }
++ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
++ }
++
++ public boolean hasUpdates() {
++ return !this.lightQueue.isEmpty();
++ }
++
++ public Level getWorld() {
++ return this.world;
++ }
++
++ public LightChunkGetter getLightAccess() {
++ return this.lightAccess;
++ }
++
++ public SkyStarLightEngine getSkyLightEngine() {
++ if (this.cachedSkyPropagators == null) {
++ return null;
++ }
++ final SkyStarLightEngine ret;
++ synchronized (this.cachedSkyPropagators) {
++ ret = this.cachedSkyPropagators.pollFirst();
++ }
++
++ if (ret == null) {
++ return new SkyStarLightEngine(this.world);
++ }
++ return ret;
++ }
++
++ public void releaseSkyLightEngine(final SkyStarLightEngine engine) {
++ if (this.cachedSkyPropagators == null) {
++ return;
++ }
++ synchronized (this.cachedSkyPropagators) {
++ this.cachedSkyPropagators.addFirst(engine);
++ }
++ }
++
++ public BlockStarLightEngine getBlockLightEngine() {
++ if (this.cachedBlockPropagators == null) {
++ return null;
++ }
++ final BlockStarLightEngine ret;
++ synchronized (this.cachedBlockPropagators) {
++ ret = this.cachedBlockPropagators.pollFirst();
++ }
++
++ if (ret == null) {
++ return new BlockStarLightEngine(this.world);
++ }
++ return ret;
++ }
++
++ public void releaseBlockLightEngine(final BlockStarLightEngine engine) {
++ if (this.cachedBlockPropagators == null) {
++ return;
++ }
++ synchronized (this.cachedBlockPropagators) {
++ this.cachedBlockPropagators.addFirst(engine);
++ }
++ }
++
++ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
++ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
++ return null;
++ }
++
++ return this.lightQueue.queueBlockChange(pos);
++ }
++
++ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
++ if (this.world == null) { // empty world
++ return null;
++ }
++
++ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
++ }
++
++ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
++
++ try {
++ if (skyEngine != null) {
++ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
++ }
++ if (blockEngine != null) {
++ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
++ }
++ } finally {
++ this.releaseSkyLightEngine(skyEngine);
++ this.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
++
++ try {
++ if (skyEngine != null) {
++ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
++ }
++ if (blockEngine != null) {
++ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
++ }
++ } finally {
++ this.releaseSkyLightEngine(skyEngine);
++ this.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
++
++ try {
++ if (skyEngine != null) {
++ skyEngine.light(this.lightAccess, chunk, emptySections);
++ }
++ if (blockEngine != null) {
++ blockEngine.light(this.lightAccess, chunk, emptySections);
++ }
++ } finally {
++ this.releaseSkyLightEngine(skyEngine);
++ this.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
++ final IntConsumer onComplete) {
++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
++
++ try {
++ if (skyEngine != null) {
++ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
++ blockEngine == null ? onComplete : null);
++ }
++ if (blockEngine != null) {
++ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
++ }
++ } finally {
++ this.releaseSkyLightEngine(skyEngine);
++ this.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ public void checkChunkEdges(final int chunkX, final int chunkZ) {
++ this.checkSkyEdges(chunkX, chunkZ);
++ this.checkBlockEdges(chunkX, chunkZ);
++ }
++
++ public void checkSkyEdges(final int chunkX, final int chunkZ) {
++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
++
++ try {
++ if (skyEngine != null) {
++ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
++ }
++ } finally {
++ this.releaseSkyLightEngine(skyEngine);
++ }
++ }
++
++ public void checkBlockEdges(final int chunkX, final int chunkZ) {
++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
++ try {
++ if (blockEngine != null) {
++ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
++ }
++ } finally {
++ this.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ public void propagateChanges() {
++ final LightQueue lightQueue = this.lightQueue;
++ if (lightQueue instanceof ClientLightQueue clientLightQueue) {
++ clientLightQueue.drainTasks();
++ } // else: invalid usage, although we won't throw because mods...
++ }
++
++ public static abstract class LightQueue {
++
++ protected final StarLightInterface lightInterface;
++
++ public LightQueue(final StarLightInterface lightInterface) {
++ this.lightInterface = lightInterface;
++ }
++
++ public abstract boolean isEmpty();
++
++ public abstract ChunkTasks queueBlockChange(final BlockPos pos);
++
++ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue);
++
++ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections);
++
++ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections);
++
++ public static abstract class ChunkTasks implements Runnable {
++
++ public final long chunkCoordinate;
++
++ protected final StarLightInterface lightEngine;
++ protected final LightQueue queue;
++ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>();
++ protected final Set<BlockPos> changedPositions = new HashSet<>();
++ protected Boolean[] changedSectionSet;
++ protected ShortOpenHashSet queuedEdgeChecksSky;
++ protected ShortOpenHashSet queuedEdgeChecksBlock;
++ protected List<BooleanSupplier> lightTasks;
++
++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) {
++ this.chunkCoordinate = chunkCoordinate;
++ this.lightEngine = lightEngine;
++ this.queue = queue;
++ }
++
++ @Override
++ public abstract void run();
++
++ public void queueOrRunTask(final Runnable run) {
++ if (!this.onComplete.add(run)) {
++ run.run();
++ }
++ }
++
++ protected void addChangedPosition(final BlockPos pos) {
++ this.changedPositions.add(pos.immutable());
++ }
++
++ protected void setChangedSection(final int y, final Boolean newEmptyValue) {
++ if (this.changedSectionSet == null) {
++ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1];
++ }
++ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue;
++ }
++
++ protected void addLightTask(final BooleanSupplier lightTask) {
++ if (this.lightTasks == null) {
++ this.lightTasks = new ArrayList<>();
++ }
++ this.lightTasks.add(lightTask);
++ }
++
++ protected void addEdgeChecksSky(final ShortCollection values) {
++ if (this.queuedEdgeChecksSky == null) {
++ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size()));
++ }
++ this.queuedEdgeChecksSky.addAll(values);
++ }
++
++ protected void addEdgeChecksBlock(final ShortCollection values) {
++ if (this.queuedEdgeChecksBlock == null) {
++ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size()));
++ }
++ this.queuedEdgeChecksBlock.addAll(values);
++ }
++
++ protected final void runTasks() {
++ boolean litChunk = false;
++ if (this.lightTasks != null) {
++ for (final BooleanSupplier run : this.lightTasks) {
++ if (run.getAsBoolean()) {
++ litChunk = true;
++ break;
++ }
+ }
++ }
+
-+ if (predicate != null && !predicate.test((T)entity)) {
-+ continue;
++ if (!litChunk) {
++ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine();
++ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine();
++ try {
++ final long coordinate = this.chunkCoordinate;
++ final int chunkX = CoordinateUtils.getChunkX(coordinate);
++ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
++
++ final Set<BlockPos> positions = this.changedPositions;
++ final Boolean[] sectionChanges = this.changedSectionSet;
++
++ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
++ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
++ }
++ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
++ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
++ }
++
++ if (skyEngine != null && this.queuedEdgeChecksSky != null) {
++ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky);
++ }
++ if (blockEngine != null && this.queuedEdgeChecksBlock != null) {
++ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock);
++ }
++ } finally {
++ this.lightEngine.releaseSkyLightEngine(skyEngine);
++ this.lightEngine.releaseBlockLightEngine(blockEngine);
++ }
++ }
++
++ Runnable run;
++ while ((run = this.onComplete.pollOrBlockAdds()) != null) {
++ run.run();
++ }
++ }
++ }
++ }
++
++ public static final class ClientLightQueue extends LightQueue {
++
++ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
++
++ public ClientLightQueue(final StarLightInterface lightInterface) {
++ super(lightInterface);
++ }
++
++ @Override
++ public synchronized boolean isEmpty() {
++ return this.chunkTasks.isEmpty();
++ }
++
++ // must hold synchronized lock on this object
++ private ClientChunkTasks getOrCreate(final long key) {
++ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> {
++ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this);
++ });
++ }
++
++ @Override
++ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) {
++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
++ tasks.addChangedPosition(pos);
++ return tasks;
++ }
++
++ @Override
++ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
++
++ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
++
++ return tasks;
++ }
++
++ @Override
++ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
++
++ tasks.addEdgeChecksSky(sections);
++
++ return tasks;
++ }
++
++ @Override
++ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
++
++ tasks.addEdgeChecksBlock(sections);
++
++ return tasks;
++ }
++
++ public synchronized ClientChunkTasks removeFirstTask() {
++ if (this.chunkTasks.isEmpty()) {
++ return null;
++ }
++ return this.chunkTasks.removeFirst();
++ }
++
++ public void drainTasks() {
++ ClientChunkTasks task;
++ while ((task = this.removeFirstTask()) != null) {
++ task.runTasks();
++ }
++ }
++
++ public static final class ClientChunkTasks extends ChunkTasks {
++
++ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) {
++ super(chunkCoordinate, lightEngine, queue);
++ }
++
++ @Override
++ public void run() {
++ this.runTasks();
++ }
++ }
++ }
++
++ public static final class ServerLightQueue extends LightQueue {
++
++ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
++
++ public ServerLightQueue(final StarLightInterface lightInterface) {
++ super(lightInterface);
++ }
++
++ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (task != null) {
++ task.lowerPriority(priority);
++ }
++ }
++
++ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (task != null) {
++ task.setPriority(priority);
++ }
++ }
++
++ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (task != null) {
++ task.raisePriority(priority);
++ }
++ }
++
++ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) {
++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (task != null) {
++ return task.getPriority();
++ }
++
++ return PrioritisedExecutor.Priority.COMPLETING;
++ }
++
++ @Override
++ public boolean isEmpty() {
++ return this.chunkTasks.isEmpty();
++ }
++
++ @Override
++ public ServerChunkTasks queueBlockChange(final BlockPos pos) {
++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
++ if (valueInMap == null) {
++ valueInMap = new ServerChunkTasks(
++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
++ );
++ }
++ valueInMap.addChangedPosition(pos);
++ return valueInMap;
++ });
++
++ ret.schedule();
++
++ return ret;
++ }
++
++ @Override
++ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
++ if (valueInMap == null) {
++ valueInMap = new ServerChunkTasks(
++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
++ );
++ }
++
++ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
++
++ return valueInMap;
++ });
++
++ ret.schedule();
++
++ return ret;
++ }
++
++ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
++ if (valueInMap == null) {
++ valueInMap = new ServerChunkTasks(
++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority
++ );
++ }
++
++ valueInMap.addLightTask(lightTask);
++
++ return valueInMap;
++ });
++
++ ret.schedule();
++
++ return ret;
++ }
++
++ @Override
++ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
++ if (valueInMap == null) {
++ valueInMap = new ServerChunkTasks(
++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
++ );
++ }
++
++ valueInMap.addEdgeChecksSky(sections);
++
++ return valueInMap;
++ });
++
++ ret.schedule();
++
++ return ret;
++ }
++
++ @Override
++ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
++ if (valueInMap == null) {
++ valueInMap = new ServerChunkTasks(
++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
++ );
++ }
++
++ valueInMap.addEdgeChecksBlock(sections);
++
++ return valueInMap;
++ });
++
++ ret.schedule();
++
++ return ret;
++ }
++
++ public static final class ServerChunkTasks extends ChunkTasks {
++
++ private final AtomicBoolean ticketAdded = new AtomicBoolean();
++ private final PrioritisedExecutor.PrioritisedTask task;
++
++ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
++ final ServerLightQueue queue) {
++ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
++ }
++
++ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
++ final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) {
++ super(chunkCoordinate, lightEngine, queue);
++ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask(
++ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate),
++ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority
++ );
++ }
++
++ public boolean markTicketAdded() {
++ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true);
++ }
++
++ public void schedule() {
++ this.task.queue();
++ }
++
++ public boolean cancel() {
++ return this.task.cancel();
++ }
++
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.task.getPriority();
++ }
++
++ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ this.task.lowerPriority(priority);
++ }
++
++ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ this.task.setPriority(priority);
++ }
++
++ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ this.task.raisePriority(priority);
++ }
++
++ @Override
++ public void run() {
++ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this);
++
++ this.runTasks();
++ }
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..39ec319d0899a6e26f29fa7a6ead4bda78ecbaeb
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
+@@ -0,0 +1,20 @@
++package ca.spottedleaf.moonrise.patches.starlight.light;
++
++import net.minecraft.core.SectionPos;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.LightLayer;
++import net.minecraft.world.level.chunk.DataLayer;
++import net.minecraft.world.level.chunk.LevelChunk;
++
++public interface StarLightLightingProvider {
++
++ public StarLightInterface starlight$getLightEngine();
++
++ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
++ final DataLayer nibble, final boolean trustEdges);
++
++ public void starlight$clientRemoveLightData(final ChunkPos chunkPos);
++
++ public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk);
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b71a80d3f
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
+@@ -0,0 +1,188 @@
++package ca.spottedleaf.moonrise.patches.starlight.util;
++
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
++import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
++import com.mojang.logging.LogUtils;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import org.slf4j.Logger;
++
++public final class SaveUtil {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ private static final int STARLIGHT_LIGHT_VERSION = 9;
++
++ public static int getLightVersion() {
++ return STARLIGHT_LIGHT_VERSION;
++ }
++
++ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
++ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
++ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
++
++ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
++ try {
++ saveLightHookReal(world, chunk, nbt);
++ } catch (final Throwable ex) {
++ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
++ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
++ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
++ }
++ }
++
++ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
++ if (tag == null) {
++ return;
++ }
++
++ final int minSection = WorldUtil.getMinLightSection(world);
++ final int maxSection = WorldUtil.getMaxLightSection(world);
++
++ SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles();
++ SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
++
++ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
++ // diff start - store our tag for whether light data is init'd
++ if (lit) {
++ tag.putBoolean("isLightOn", false);
++ }
++ // diff end - store our tag for whether light data is init'd
++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
++
++ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
++
++ ListTag sectionsStored = tag.getList("sections", 10);
++
++ for (int i = 0; i < sectionsStored.size(); ++i) {
++ CompoundTag sectionStored = sectionsStored.getCompound(i);
++ int k = sectionStored.getByte("Y");
++
++ // strip light data
++ sectionStored.remove("BlockLight");
++ sectionStored.remove("SkyLight");
++
++ if (!sectionStored.isEmpty()) {
++ sections[k - minSection] = sectionStored;
++ }
++ }
++
++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
++ for (int i = minSection; i <= maxSection; ++i) {
++ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
++ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
++ if (blockNibble != null || skyNibble != null) {
++ CompoundTag section = sections[i - minSection];
++ if (section == null) {
++ section = new CompoundTag();
++ section.putByte("Y", (byte)i);
++ sections[i - minSection] = section;
++ }
++
++ // we store under the same key so mod programs editing nbt
++ // can still read the data, hopefully.
++ // however, for compatibility we store chunks as unlit so vanilla
++ // is forced to re-light them if it encounters our data. It's too much of a burden
++ // to try and maintain compatibility with a broken and inferior skylight management system.
++
++ if (blockNibble != null) {
++ if (blockNibble.data != null) {
++ section.putByteArray("BlockLight", blockNibble.data);
++ }
++ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
+ }
+
-+ into.add((T)entity);
++ if (skyNibble != null) {
++ if (skyNibble.data != null) {
++ section.putByteArray("SkyLight", skyNibble.data);
++ }
++ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
++ }
++ }
++ }
++ }
++
++ // rewrite section list
++ sectionsStored.clear();
++ for (CompoundTag section : sections) {
++ if (section != null) {
++ sectionsStored.add(section);
++ }
++ }
++ tag.put("sections", sectionsStored);
++ if (lit) {
++ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
++ }
++ }
++
++ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
++ try {
++ loadLightHookReal(world, pos, tag, into);
++ } catch (final Throwable ex) {
++ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
++ // lighting in both cases.
++ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
++ }
++ }
++
++ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
++ if (into == null) {
++ return;
++ }
++ final int minSection = WorldUtil.getMinLightSection(world);
++ final int maxSection = WorldUtil.getMaxLightSection(world);
++
++ into.setLightCorrect(false); // mark as unlit in case we fail parsing
++
++ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
++ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
++
++
++ // start copy from the original method
++ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
++ boolean canReadSky = world.dimensionType().hasSkyLight();
++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
++ ListTag sections = tag.getList("sections", 10);
++
++ for (int i = 0; i < sections.size(); ++i) {
++ CompoundTag sectionData = sections.getCompound(i);
++ int y = sectionData.getByte("Y");
++
++ if (sectionData.contains("BlockLight", 7)) {
++ // this is where our diff is
++ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
++ } else {
++ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
++ }
++
++ if (canReadSky) {
++ if (sectionData.contains("SkyLight", 7)) {
++ // we store under the same key so mod programs editing nbt
++ // can still read the data, hopefully.
++ // however, for compatibility we store chunks as unlit so vanilla
++ // is forced to re-light them if it encounters our data. It's too much of a burden
++ // to try and maintain compatibility with a broken and inferior skylight management system.
++ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
++ } else {
++ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
++ }
+ }
+ }
+ }
++ // end copy from vanilla
++
++ ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles);
++ ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles);
++ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
++ }
++
++ private SaveUtil() {}
++}
+diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+index 0d0cb3e63acd5156b6f9d6d78cc949b0af36a77b..dd1649abe57d7191c15a9b2862d5fd11ff0706b1 100644
+--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+@@ -25,6 +25,10 @@ import java.util.List;
+ import java.util.concurrent.CompletableFuture;
+ import java.util.function.Consumer;
+
++/**
++ * @deprecated Use {@link ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem}
++ */
++@Deprecated(forRemoval = true)
+ public final class ChunkSystem {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+@@ -35,31 +39,17 @@ public final class ChunkSystem {
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run); // Paper - reroute
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
+- level.chunkSource.mainThreadProcessor.execute(run);
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run, priority); // Paper - reroute
+ }
+
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+ final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
+ final Consumer<ChunkAccess> onComplete) {
+- if (gen) {
+- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+- return;
+- }
+- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+- if (chunk == null) {
+- onComplete.accept(null);
+- } else {
+- if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+- } else {
+- onComplete.accept(null);
+- }
+- }
+- });
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - reroute
+ }
+
+ static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
+@@ -67,164 +57,29 @@ public final class ChunkSystem {
+ private static long chunkLoadCounter = 0L;
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
+- if (!Bukkit.isPrimaryThread()) {
+- scheduleChunkTask(level, chunkX, chunkZ, () -> {
+- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+- }, priority);
+- return;
+- }
+-
+- final int minLevel = 33 + ChunkSystem.getDistance(toStatus);
+- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
+-
+- if (addTicket) {
+- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+- }
+- level.chunkSource.runDistanceManagerUpdates();
+-
+- final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+- try {
+- if (onComplete != null) {
+- onComplete.accept(chunk);
+- }
+- } catch (final ThreadDeath death) {
+- throw death;
+- } catch (final Throwable thr) {
+- LOGGER.error("Exception handling chunk load callback", thr);
+- SneakyThrow.sneaky(thr);
+- } finally {
+- if (addTicket) {
+- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+- }
+- }
+- };
+-
+- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-
+- if (holder == null || holder.getTicketLevel() > minLevel) {
+- loadCallback.accept(null);
+- return;
+- }
+-
+- final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
+-
+- if (loadFuture.isDone()) {
+- loadCallback.accept(loadFuture.join().orElse(null));
+- return;
+- }
+-
+- loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
+- if (thr != null) {
+- loadCallback.accept(null);
+- return;
+- }
+- loadCallback.accept(result.orElse(null));
+- }, (final Runnable r) -> {
+- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+- });
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
+ }
+
+ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+ final FullChunkStatus toStatus, final boolean addTicket,
+ final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
+- // This method goes unused until the chunk system rewrite
+- if (toStatus == FullChunkStatus.INACCESSIBLE) {
+- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+- }
+-
+- if (!Bukkit.isPrimaryThread()) {
+- scheduleChunkTask(level, chunkX, chunkZ, () -> {
+- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+- }, priority);
+- return;
+- }
+-
+- final int minLevel = 33 - (toStatus.ordinal() - 1);
+- final int radius = toStatus.ordinal() - 1;
+- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
+-
+- if (addTicket) {
+- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+- }
+- level.chunkSource.runDistanceManagerUpdates();
+-
+- final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+- try {
+- if (onComplete != null) {
+- onComplete.accept(chunk);
+- }
+- } catch (final ThreadDeath death) {
+- throw death;
+- } catch (final Throwable thr) {
+- LOGGER.error("Exception handling chunk load callback", thr);
+- SneakyThrow.sneaky(thr);
+- } finally {
+- if (addTicket) {
+- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+- }
+- }
+- };
+-
+- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-
+- if (holder == null || holder.getTicketLevel() > minLevel) {
+- loadCallback.accept(null);
+- return;
+- }
+-
+- final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
+- switch (toStatus) {
+- case FULL: {
+- tickingState = holder.getFullChunkFuture();
+- break;
+- }
+- case BLOCK_TICKING: {
+- tickingState = holder.getTickingChunkFuture();
+- break;
+- }
+- case ENTITY_TICKING: {
+- tickingState = holder.getEntityTickingChunkFuture();
+- break;
+- }
+- default: {
+- throw new IllegalStateException("Cannot reach here");
+- }
+- }
+-
+- if (tickingState.isDone()) {
+- loadCallback.accept(tickingState.join().orElse(null));
+- return;
+- }
+-
+- tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
+- if (thr != null) {
+- loadCallback.accept(null);
+- return;
+- }
+- loadCallback.accept(result.orElse(null));
+- }, (final Runnable r) -> {
+- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+- });
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
+ }
+
+ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolders(level); // Paper - reroute
+ }
+
+ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolders(level); // Paper - reroute
+ }
+
+ public static int getVisibleChunkHolderCount(final ServerLevel level) {
+- return level.chunkSource.chunkMap.visibleChunkMap.size();
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolderCount(level); // Paper - reroute
+ }
+
+ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+- return level.chunkSource.chunkMap.updatingChunkMap.size();
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolderCount(level); // Paper - reroute
+ }
+
+ public static boolean hasAnyChunkHolders(final ServerLevel level) {
+@@ -274,27 +129,19 @@ public final class ChunkSystem {
+ }
+
+ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUnloadingChunkHolder(level, chunkX, chunkZ); // Paper - reroute
+ }
+
+ public static int getSendViewDistance(final ServerPlayer player) {
+- return getLoadViewDistance(player);
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - reroute
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+- final ServerLevel level = player.serverLevel();
+- if (level == null) {
+- return Bukkit.getViewDistance();
+- }
+- return level.chunkSource.chunkMap.getPlayerViewDistance(player);
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getLoadViewDistance(player); // Paper - reroute
+ }
+
+ public static int getTickViewDistance(final ServerPlayer player) {
+- final ServerLevel level = player.serverLevel();
+- if (level == null) {
+- return Bukkit.getSimulationDistance();
+- }
+- return level.chunkSource.chunkMap.distanceManager.simulationDistance;
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getTickViewDistance(player); // Paper - reroute
+ }
+
+ private ChunkSystem() {
+diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+index 4de88f74182bb596c6b5ad0351cc0dacefd0ce96..b4fa4259f66e449e754a70a7a99b9e68d9eb5016 100644
+--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart {
+ public static GlobalConfiguration get() {
+ return instance;
+ }
++
++ public ChunkLoadingBasic chunkLoadingBasic;
++
++ public class ChunkLoadingBasic extends ConfigurationPart {
++ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
++ public double playerMaxChunkSendRate = 75.0;
++
++ @Comment(
++ "The maximum rate at which chunks will load for any individual player. " +
++ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
++ "chunk is already generated. Set to -1 to disable this limit."
++ )
++ public double playerMaxChunkLoadRate = 100.0;
++
++ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
++ public double playerMaxChunkGenerateRate = -1.0;
++ }
++
++ public ChunkLoadingAdvanced chunkLoadingAdvanced;
++
++ public class ChunkLoadingAdvanced extends ConfigurationPart {
++ @Comment(
++ "Set to true if the server will match the chunk send radius that clients have configured" +
++ "in their view distance settings if the client is less-than the server's send distance."
++ )
++ public boolean autoConfigSendDistance = true;
++
++ @Comment(
++ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
++ )
++ public int playerMaxConcurrentChunkLoads = 0;
++
++ @Comment(
++ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
++ )
++ public int playerMaxConcurrentChunkGenerates = 0;
++ }
+ static void set(GlobalConfiguration instance) {
+ GlobalConfiguration.instance = instance;
+ }
+@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart {
+ public int incomingPacketThreshold = 300;
+ }
+
+- public ChunkLoading chunkLoading;
+-
+- public class ChunkLoading extends ConfigurationPart {
+- public int minLoadRadius = 2;
+- public int maxConcurrentSends = 2;
+- public boolean autoconfigSendDistance = true;
+- public double targetPlayerChunkSendRate = 100.0;
+- public double globalMaxChunkSendRate = -1.0;
+- public boolean enableFrustumPriority = false;
+- public double globalMaxChunkLoadRate = -1.0;
+- public double playerMaxConcurrentLoads = 20.0;
+- public double globalMaxConcurrentLoads = 500.0;
+- public double playerMaxChunkLoadRate = -1.0;
+- }
+-
+ public UnsupportedSettings unsupportedSettings;
+
+ public class UnsupportedSettings extends ConfigurationPart {
+@@ -201,7 +225,7 @@ public class GlobalConfiguration extends ConfigurationPart {
+
+ @PostProcess
+ private void postProcess() {
+- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
+ }
+ }
+
+diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9d04285165241baec1005cb3ae81a623bcd3945a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
+@@ -0,0 +1,10 @@
++package io.papermc.paper.threadedregions;
++
++// placeholder class for Folia
++public class TickRegions {
++
++ public static int getRegionChunkShift() {
++ return 2;
+ }
++
+}
-diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
-index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644
---- a/src/main/java/net/minecraft/server/Main.java
-+++ b/src/main/java/net/minecraft/server/Main.java
-@@ -320,6 +320,7 @@ public class Main {
-
- convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
- */
-+ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async
- final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
- DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
+diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
+index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..d05297d77147ab68f8c5bb08f13a1f882a686c4f 100644
+--- a/src/main/java/io/papermc/paper/util/TickThread.java
++++ b/src/main/java/io/papermc/paper/util/TickThread.java
+@@ -6,7 +6,7 @@ import net.minecraft.world.entity.Entity;
+ import org.bukkit.Bukkit;
+ import java.util.concurrent.atomic.AtomicInteger;
+
+-public final class TickThread extends Thread {
++public class TickThread extends Thread {
+
+ public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks");
+@@ -16,6 +16,10 @@ public final class TickThread extends Thread {
+ }
+ }
+
++ /**
++ * @deprecated
++ */
++ @Deprecated
+ public static void softEnsureTickThread(final String reason) {
+ if (!STRICT_THREAD_CHECKS) {
+ return;
+@@ -23,6 +27,10 @@ public final class TickThread extends Thread {
+ ensureTickThread(reason);
+ }
+
++ /**
++ * @deprecated
++ */
++ @Deprecated
+ public static void ensureTickThread(final String reason) {
+ if (!isTickThread()) {
+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+@@ -30,6 +38,21 @@ public final class TickThread extends Thread {
+ }
+ }
+
++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.core.BlockPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++
+ public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
+ if (!isTickThreadFor(world, chunkX, chunkZ)) {
+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+@@ -44,6 +67,21 @@ public final class TickThread extends Thread {
+ }
+ }
+
++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.phys.AABB aabb, final String reason) {
++ if (!isTickThreadFor(world, aabb)) {
++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
++ if (!isTickThreadFor(world, blockX, blockZ)) {
++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
++ }
++
++
+ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+@@ -66,13 +104,45 @@ public final class TickThread extends Thread {
+ }
+
+ public static boolean isTickThread() {
+- return Bukkit.isPrimaryThread();
++ return Thread.currentThread() instanceof TickThread;
++ }
++
++ public static boolean isShutdownThread() {
++ return false;
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.core.BlockPos pos) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 pos) {
++ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
+ return isTickThread();
+ }
+
++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.AABB aabb) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 position, final net.minecraft.world.phys.Vec3 deltaMovement, final int buffer) {
++ return isTickThread();
++ }
++
++ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
++ return isTickThread();
++ }
++
+ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
+ return isTickThread();
+ }
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 1a9e323659dcff12ce53919eb3d6d6f66f835292..2e4f20ba5f6f61b797f1eef267302fa3314f94a5 100644
+index 6915522f669631779c1fb8a8e2db330f4b9fb921..cd69971065b13353353eca55f6e145949390de11 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -315,7 +315,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -198,7 +198,7 @@ import org.bukkit.event.server.ServerLoadEvent;
+
+ import co.aikar.timings.MinecraftTimings; // Paper
+
+-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable {
++public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system
+
+ private static MinecraftServer SERVER; // Paper
+ public static final Logger LOGGER = LogUtils.getLogger();
+@@ -322,7 +322,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
AtomicReference<S> atomicreference = new AtomicReference();
@@ -15642,73 +22362,65 @@ index 1a9e323659dcff12ce53919eb3d6d6f66f835292..2e4f20ba5f6f61b797f1eef267302fa3
((MinecraftServer) atomicreference.get()).runServer();
}, "Server thread");
-@@ -651,7 +651,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -341,6 +341,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ return s0;
+ }
+
++ // Paper start - rewrite chunk system
++ private volatile Throwable chunkSystemCrash;
++
++ @Override
++ public final void moonrise$setChunkSystemCrash(final Throwable throwable) {
++ this.chunkSystemCrash = throwable;
++ }
++ // Paper end - rewrite chunk system
++
+ public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
+ super("Server");
+ SERVER = this; // Paper - better singleton
+@@ -657,7 +666,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.forceDifficulty();
for (ServerLevel worldserver : this.getAllLevels()) {
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
-+ //worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system, not required to "tick" anything
++ // Paper - rewrite chunk system
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
}
-@@ -861,6 +861,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -870,6 +879,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public abstract boolean shouldRconBroadcast();
public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) {
-+ // Paper start - rewrite chunk system - add close param
-+ // This allows us to avoid double saving chunks by closing instead of saving then closing
++ // Paper start - add close param
+ return this.saveAllChunks(suppressLogs, flush, force, false);
+ }
+ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) {
-+ // Paper end - rewrite chunk system - add close param
++ // Paper end - add close param
boolean flag3 = false;
for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
-@@ -869,8 +875,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- if (!suppressLogs) {
+@@ -879,7 +893,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
}
--
+
- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
-+ // Paper start - rewrite chunk system
-+ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close);
-+ if (flush) {
-+ MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", worldserver.getChunkSource().chunkMap.getStorageName());
-+ }
-+ // Paper end - rewrite chunk system
++ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param
}
// CraftBukkit start - moved to WorldServer.save
-@@ -889,7 +899,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- while (iterator1.hasNext()) {
- ServerLevel worldserver2 = (ServerLevel) iterator1.next();
-
-- MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", worldserver2.getChunkSource().chunkMap.getStorageName());
-+ //MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", worldserver2.getChunkSource().chunkMap.getStorageName()); // Paper - move up
- }
-
- MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage: All dimensions are saved");
-@@ -971,36 +981,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -980,7 +994,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- while (this.levels.values().stream().anyMatch((worldserver1) -> {
-- return worldserver1.getChunkSource().chunkMap.hasWork();
-- })) {
-- this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
-- iterator = this.getAllLevels().iterator();
--
-- while (iterator.hasNext()) {
-- worldserver = (ServerLevel) iterator.next();
-- worldserver.getChunkSource().removeTicketsOnClosing();
-- worldserver.getChunkSource().tick(() -> {
-- return true;
-- }, false);
-- }
--
-- this.waitUntilNextTick();
-- }
--
++ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system
+ return worldserver1.getChunkSource().chunkMap.hasWork();
+ })) {
+ this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
+@@ -997,19 +1011,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.waitUntilNextTick();
+ }
+
- this.saveAllChunks(false, true, false);
- iterator = this.getAllLevels().iterator();
-
@@ -15722,269 +22434,210 @@ index 1a9e323659dcff12ce53919eb3d6d6f66f835292..2e4f20ba5f6f61b797f1eef267302fa3
- }
- }
- }
-+ this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system - move closing into here
++ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system
this.isSaving = false;
this.resources.close();
-@@ -1020,6 +1001,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1029,6 +1031,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// Spigot end
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.close(true); // Paper - rewrite chunk system
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system
}
public String getLocalIp() {
-@@ -1112,6 +1094,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- // Paper end
- // Spigot End
-
-+ public static volatile RuntimeException chunkSystemCrash; // Paper - rewrite chunk system
-+
- protected void runServer() {
- try {
- if (!this.initServer()) {
-@@ -1140,6 +1124,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- // Paper end - Add onboarding message for initial server start
-
- while (this.running) {
+@@ -1203,6 +1206,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ this.tickServer(flag ? () -> {
+ return false;
+ } : this::haveTime);
+ // Paper start - rewrite chunk system
-+ // guarantee that nothing can stop the server from halting if it can at least still tick
-+ if (this.chunkSystemCrash != null) {
-+ throw this.chunkSystemCrash;
++ final Throwable crash = this.chunkSystemCrash;
++ if (crash != null) {
++ this.chunkSystemCrash = null;
++ throw new RuntimeException("Chunk system crash propagated to tick()", crash);
+ }
+ // Paper end - rewrite chunk system
- long i;
-
- if (!this.isPaused() && this.tickRateManager.isSprinting() && this.tickRateManager.checkShouldSprintThisTick()) {
-@@ -1302,6 +1292,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- }
-
- private boolean haveTime() {
-+ // Paper start
-+ if (this.forceTicks) {
-+ return true;
-+ }
-+ // Paper end
- // CraftBukkit start
- if (isOversleep) return canOversleep(); // Paper - because of our changes, this logic is broken
- return this.forceTicks || this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos);
-@@ -1564,7 +1559,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- // Paper start - Folia scheduler API
- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick();
- getAllLevels().forEach(level -> {
-- for (final Entity entity : level.getEntities().getAll()) {
-+ for (final Entity entity : level.getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system
- if (entity.isRemoved()) {
- continue;
- }
-@@ -2622,6 +2617,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-
- }
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public boolean isSameThread() {
-+ return io.papermc.paper.util.TickThread.isTickThread();
-+ }
-+ // Paper end - rewrite chunk system
-+
- // CraftBukkit start
- public boolean isDebugging() {
- return false;
-diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-index ea5ef39a814522f0abffd570e216d899833f588d..e365ed1be9739f57d0e1851f0593229dc1286796 100644
---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -473,7 +473,34 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
- return this.getProperties().allowNether;
- }
-
-+ static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system
-+
- public void handleConsoleInput(String command, CommandSourceStack commandSource) {
-+ // Paper start - rewrite chunk system
-+ if (command.equalsIgnoreCase("paper debug chunks --async")) {
-+ LOGGER.info("Scheduling async debug chunks");
-+ Runnable run = () -> {
-+ LOGGER.info("Async debug chunks executing");
-+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(false);
-+ CommandSender sender = MinecraftServer.getServer().console;
-+ java.io.File file = new java.io.File(new java.io.File(new java.io.File("."), "debug"),
-+ "chunks-" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(java.time.LocalDateTime.now()) + ".txt");
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN));
-+ try {
-+ io.papermc.paper.util.MCUtil.dumpChunks(file, true);
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN));
-+ } catch (Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED));
-+ }
-+ };
-+ Thread t = new Thread(run);
-+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement());
-+ t.setDaemon(true);
-+ t.start();
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
- this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
- }
-
+ this.profiler.popPush("nextTickWait");
+ this.mayHaveDelayedTasks = true;
+ this.delayedTasksMaxNextTickTimeNanos = Math.max(Util.getNanos() + i, this.nextTickTimeNanos);
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbbf53353a0 100644
+index 09d7b416c02eb13c506e9dc92d78e983bf43f4f0..963a591e1031834cb6e98b5ed6f2ef307ba1ae78 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -46,17 +46,12 @@ public class ChunkHolder {
- public static final ChunkResult<ChunkAccess> NOT_DONE_YET = ChunkResult.error("Not done yet");
+@@ -32,28 +32,20 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
+ import net.minecraft.server.MinecraftServer;
+ // CraftBukkit end
+
+-public class ChunkHolder extends GenerationChunkHolder {
++public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system
+
+ public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
- private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
-- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures;
-+ // Paper - rewrite chunk system
private final LevelHeightAccessor levelHeightAccessor;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
-- public CompletableFuture<ChunkAccess> chunkToSave; // Paper - public
-+ // Paper - rewrite chunk system
- @Nullable
- private final DebugBuffer<ChunkHolder.ChunkSaveDebug> chunkToSaveHistory;
- public int oldTicketLevel;
- private int ticketLevel;
- private int queueLevel;
+ // Paper - rewrite chunk system
- public final ChunkPos pos;
private boolean hasChangedSections;
private final ShortSet[] changedBlocksPerSection;
-@@ -65,11 +60,20 @@ public class ChunkHolder {
+ private final BitSet blockChangedLightSectionFilter;
+ private final BitSet skyChangedLightSectionFilter;
private final LevelLightEngine lightEngine;
- private final ChunkHolder.LevelChangeListener onLevelChange;
+- private final ChunkHolder.LevelChangeListener onLevelChange;
++ // Paper - rewrite chunk system
public final ChunkHolder.PlayerProvider playerProvider;
- private boolean wasAccessibleSinceLastSave;
-- private CompletableFuture<Void> pendingFullStateConfirmation;
+- private CompletableFuture<?> pendingFullStateConfirmation;
- private CompletableFuture<?> sendSync;
+- private CompletableFuture<?> saveSync;
+ // Paper - rewrite chunk system
private final ChunkMap chunkMap; // Paper
-+ // Paper start - no-tick view distance
-+ public final LevelChunk getSendingChunk() {
-+ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used
-+ // in Chunk's neighbour callback
-+ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z);
-+ if (ret != null && ret.areNeighboursLoaded(1)) {
-+ return ret;
-+ }
-+ return null;
-+ }
-+ // Paper end - no-tick view distance
- // Paper start
- public void onChunkAdd() {
-@@ -81,147 +85,131 @@ public class ChunkHolder {
+@@ -67,23 +59,110 @@ public class ChunkHolder extends GenerationChunkHolder {
}
// Paper end
-- public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
-- this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
-- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- this.chunkToSave = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-+ public final io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder; // Paper - rewrite chunk system
++ // Paper start - rewrite chunk system
++ private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder;
+
-+ // Paper start - replace player chunk loader
-+ private final com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>();
++ private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0];
++ private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY, 0);
+
-+ public void addPlayer(ServerPlayer player) {
++ private ChunkMap getChunkMap() {
++ return (ChunkMap)this.playerProvider;
++ }
++
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() {
++ return this.newChunkHolder;
++ }
++
++ @Override
++ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) {
++ this.newChunkHolder = newChunkHolder;
++ }
++
++ @Override
++ public final void moonrise$addReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.add(player)) {
-+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player);
++ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
-+ public void removePlayer(ServerPlayer player) {
++ @Override
++ public final void moonrise$removeReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.remove(player)) {
-+ throw new IllegalStateException("Have not sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player);
++ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
-+ public boolean hasChunkBeenSent() {
++ @Override
++ public final boolean moonrise$hasChunkBeenSent() {
+ return this.playersSentChunkTo.size() != 0;
+ }
+
-+ public boolean hasBeenSent(ServerPlayer to) {
++ @Override
++ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) {
+ return this.playersSentChunkTo.contains(to);
+ }
-+ // Paper end - replace player chunk loader
-+ public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system
-+ this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system
- this.chunkToSaveHistory = null;
++
++ @Override
++ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) {
++ final List<ServerPlayer> ret = new java.util.ArrayList<>();
++ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked();
++ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
++ final ServerPlayer player = raw[i];
++ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
++ continue;
++ }
++ ret.add(player);
++ }
++
++ return ret;
++ }
++
++ @Override
++ public final LevelChunk moonrise$getFullChunk() {
++ if (this.newChunkHolder.isFullChunkReady()) {
++ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
++ return levelChunk;
++ } // else: race condition: chunk unload
++ }
++ return null;
++ }
++
++ private boolean isRadiusLoaded(final int radius) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler()
++ .chunkHolderManager;
++ final ChunkPos pos = this.pos;
++ final int chunkX = pos.x;
++ final int chunkZ = pos.z;
++ for (int dz = -radius; dz <= radius; ++dz) {
++ for (int dx = -radius; dx <= radius; ++dx) {
++ if ((dx | dz) == 0) {
++ continue;
++ }
++
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ);
++
++ if (holder == null || !holder.isFullChunkReady()) {
++ return false;
++ }
++ }
++ }
++
++ return true;
++ }
++ // Paper end - rewrite chunk system
++
+ public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
+ super(pos);
+- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
++ // Paper - rewrite chunk system
this.blockChangedLightSectionFilter = new BitSet();
this.skyChangedLightSectionFilter = new BitSet();
- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
+- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
+ // Paper - rewrite chunk system
- this.pos = pos;
this.levelHeightAccessor = world;
this.lightEngine = lightingProvider;
- this.onLevelChange = levelUpdateListener;
-+ this.onLevelChange = null; // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
this.playerProvider = playersWatchingChunkProvider;
- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
- this.ticketLevel = this.oldTicketLevel;
- this.queueLevel = this.oldTicketLevel;
-- this.setTicketLevel(level);
+ // Paper - rewrite chunk system
+ this.setTicketLevel(level);
this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper
- }
+@@ -91,15 +170,7 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper start
public @Nullable ChunkAccess getAvailableChunkNow() {
- // TODO can we just getStatusFuture(EMPTY)?
- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) {
-- CompletableFuture<ChunkResult<ChunkAccess>> future = this.getFutureIfPresentUnchecked(curr);
-- ChunkResult<ChunkAccess> either = future.getNow(null);
-- if (either == null || either.isSuccess()) {
+- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(curr);
+- if (chunkAccess == null) {
- continue;
- }
-- return either.orElseThrow(IllegalStateException::new);
+- return chunkAccess;
- }
- return null;
-+ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system
++ return this.getChunkIfPresent(ChunkStatus.EMPTY); // Paper - rewrite chunk system
}
// Paper end
// CraftBukkit start
- public LevelChunk getFullChunkNow() {
-- // Note: We use the oldTicketLevel for isLoaded checks.
-- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
-- return this.getFullChunkNowUnchecked();
-+ // Paper start - rewrite chunk system
-+ if (!this.isFullChunkReady() || !(this.getAvailableChunkNow() instanceof LevelChunk chunk)) return null; // instanceof to avoid a race condition on off-main threads
-+ return chunk;
-+ // Paper end - rewrite chunk system
- }
-
- public LevelChunk getFullChunkNowUnchecked() {
-- CompletableFuture<ChunkResult<ChunkAccess>> statusFuture = this.getFutureIfPresentUnchecked(ChunkStatus.FULL);
-- ChunkResult<ChunkAccess> either = statusFuture.getNow(null);
-- return (either == null) ? null : (LevelChunk) either.orElse(null);
-+ // Paper start - rewrite chunk system
-+ return this.getAvailableChunkNow() instanceof LevelChunk chunk ? chunk : null;
-+ // Paper end - rewrite chunk system
- }
+@@ -113,39 +184,46 @@ public class ChunkHolder extends GenerationChunkHolder {
// CraftBukkit end
- public CompletableFuture<ChunkResult<ChunkAccess>> getFutureIfPresentUnchecked(ChunkStatus leastStatus) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = (CompletableFuture) this.futures.get(leastStatus.getIndex());
--
-- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<ChunkResult<ChunkAccess>> getFutureIfPresent(ChunkStatus leastStatus) {
-- return ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(leastStatus) ? this.getFutureIfPresentUnchecked(leastStatus) : ChunkHolder.UNLOADED_CHUNK_FUTURE;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
public final CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { // Paper - final for inline
- return this.tickingChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
@@ -15992,290 +22645,172 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
public final CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { // Paper - final for inline
- return this.entityTickingChunkFuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { // Paper - final for inline
- return this.fullChunkFuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
public final LevelChunk getTickingChunk() { // Paper - final for inline
- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
+ // Paper start - rewrite chunk system
-+ if (!this.isTickingReady()) {
-+ return null;
++ if (this.newChunkHolder.isTickingReady()) {
++ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
++ return levelChunk;
++ } // else: race condition: chunk unload
+ }
-+ return (LevelChunk)this.getAvailableChunkNow();
++ return null;
+ // Paper end - rewrite chunk system
}
- public CompletableFuture<?> getChunkSendSyncFuture() {
-- return this.sendSync;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
@Nullable
public LevelChunk getChunkToSend() {
- return !this.sendSync.isDone() ? null : this.getTickingChunk();
-+ return this.getSendingChunk(); // Paper - rewrite chunk system
++ // Paper start - rewrite chunk system
++ final LevelChunk ret = this.moonrise$getFullChunk();
++ if (ret != null && this.isRadiusLoaded(1)) {
++ return ret;
++ }
++ return null;
++ // Paper end - rewrite chunk system
}
- @Nullable
- public ChunkStatus getLastAvailableStatus() {
-- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) {
-- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i);
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus);
--
-- if (((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).isSuccess()) {
-- return chunkstatus;
-- }
-- }
--
-- return null;
-+ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system
+ public CompletableFuture<?> getSendSyncFuture() {
+- return this.sendSync;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- // Paper start
- public @Nullable ChunkStatus getChunkHolderStatus() {
-- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) {
-- CompletableFuture<ChunkResult<ChunkAccess>> future = this.getFutureIfPresentUnchecked(curr);
-- ChunkResult<ChunkAccess> either = future.getNow(null);
-- if (either == null || !either.isSuccess()) {
-- continue;
-- }
-- return curr;
+ public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
+- if (this.sendSync.isDone()) {
+- this.sendSync = postProcessingFuture;
+- } else {
+- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
+- return null;
+- });
- }
--
-- return null;
-+ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+
}
+
+@@ -164,26 +242,20 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper end
- @Nullable
- public ChunkAccess getLastAvailable() {
-- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) {
-- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i);
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus);
--
-- if (!completablefuture.isCompletedExceptionally()) {
-- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null);
--
-- if (ichunkaccess != null) {
-- return ichunkaccess;
-- }
-- }
-- }
--
-- return null;
-+ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system
+ public CompletableFuture<?> getSaveSyncFuture() {
+- return this.saveSync;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
-- public final CompletableFuture<ChunkAccess> getChunkToSave() { // Paper - final for inline
-- return this.chunkToSave;
-- }
-+ // Paper - rewrite chunk system
+ public boolean isReadyForSaving() {
+- return this.getGenerationRefCount() == 0 && this.saveSync.isDone();
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void addSaveDependency(CompletableFuture<?> savingFuture) {
+- if (this.saveSync.isDone()) {
+- this.saveSync = savingFuture;
+- } else {
+- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> {
+- return null;
+- });
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+
+ }
public void blockChanged(BlockPos pos) {
- LevelChunk chunk = this.getTickingChunk();
-+ // Paper start - replace player chunk loader
-+ if (this.playersSentChunkTo.size() == 0) {
-+ return;
-+ }
-+ // Paper end - replace player chunk loader
-+ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk != null) {
int i = this.levelHeightAccessor.getSectionIndex(pos.getY());
-@@ -237,13 +225,13 @@ public class ChunkHolder {
- }
-
- public void sectionLightChanged(LightLayer lightType, int y) {
-- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) this.getFutureIfPresent(ChunkStatus.INITIALIZE_LIGHT).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse(null); // CraftBukkit - decompile error
-+ ChunkAccess ichunkaccess = this.getAvailableChunkNow(); // Paper - rewrite chunk system
+@@ -203,7 +275,7 @@ public class ChunkHolder extends GenerationChunkHolder {
if (ichunkaccess != null) {
ichunkaccess.setUnsaved(true);
- LevelChunk chunk = this.getTickingChunk();
-+ LevelChunk chunk = this.getSendingChunk(); // Paper - rewrite chunk system
++ LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
-- if (chunk != null) {
-+ if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader
+ if (chunk != null) {
int j = this.lightEngine.getMinLightSection();
- int k = this.lightEngine.getMaxLightSection();
-
-@@ -263,7 +251,7 @@ public class ChunkHolder {
-
- // Paper start - starlight
- public void broadcast(Packet<?> packet, boolean onChunkViewEdge) {
-- this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet);
-+ this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system
- }
- // Paper end - starlight
-
-@@ -273,7 +261,7 @@ public class ChunkHolder {
+@@ -229,7 +301,7 @@ public class ChunkHolder extends GenerationChunkHolder {
List list;
if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
- list = this.playerProvider.getPlayers(this.pos, true);
-+ list = this.getPlayers(true); // Paper - rewrite chunk system
++ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
if (!list.isEmpty()) {
ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter);
-@@ -285,7 +273,7 @@ public class ChunkHolder {
+@@ -241,7 +313,7 @@ public class ChunkHolder extends GenerationChunkHolder {
}
if (this.hasChangedSections) {
- list = this.playerProvider.getPlayers(this.pos, false);
-+ list = this.getPlayers(false); // Paper - rewrite chunk system
++ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
for (int i = 0; i < this.changedBlocksPerSection.length; ++i) {
ShortSet shortset = this.changedBlocksPerSection[i];
-@@ -343,75 +331,33 @@ public class ChunkHolder {
-
- }
-
-- private void broadcast(List<ServerPlayer> players, Packet<?> packet) {
-- players.forEach((entityplayer) -> {
-- entityplayer.connection.send(packet);
-- });
-- }
--
-- public CompletableFuture<ChunkResult<ChunkAccess>> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) {
-- int i = targetStatus.getIndex();
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = (CompletableFuture) this.futures.get(i);
-+ // Paper start - rewrite chunk system
-+ public List<ServerPlayer> getPlayers(boolean onlyOnWatchDistanceEdge) {
-+ List<ServerPlayer> ret = new java.util.ArrayList<>();
-
-- if (completablefuture != null) {
-- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.getNow(ChunkHolder.NOT_DONE_YET);
--
-- if (chunkresult == null) {
-- String s = String.valueOf(targetStatus);
-- String s1 = "value in future for status: " + s + " was incorrectly set to null at chunk: " + String.valueOf(this.pos);
--
-- throw chunkStorage.debugFuturesAndCreateReportedException(new IllegalStateException("null value previously set for chunk status"), s1);
-- }
--
-- if (chunkresult == ChunkHolder.NOT_DONE_YET || chunkresult.isSuccess()) {
-- return completablefuture;
-+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
-+ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i);
-+ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
-+ continue;
- }
-+ ret.add(player);
- }
+@@ -307,193 +379,40 @@ public class ChunkHolder extends GenerationChunkHolder {
-- if (ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(targetStatus)) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture1 = chunkStorage.schedule(this, targetStatus);
--
-- this.updateChunkToSave(completablefuture1, "schedule " + String.valueOf(targetStatus));
-- this.futures.set(i, completablefuture1);
-- return completablefuture1;
-- } else {
-- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture;
-- }
-+ return ret;
+ @Override
+ public final int getTicketLevel() { // Paper - final for inline
+- return this.ticketLevel;
++ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system
}
-+ // Paper end - rewrite chunk system
-- protected void addSaveDependency(String thenDesc, CompletableFuture<?> then) {
-- if (this.chunkToSaveHistory != null) {
-- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc));
-- }
-
-- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, object) -> {
-- return ichunkaccess;
-- });
-- }
--
-- private void updateChunkToSave(CompletableFuture<? extends ChunkResult<? extends ChunkAccess>> then, String thenDesc) {
-- if (this.chunkToSaveHistory != null) {
-- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc));
-- }
--
-- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, chunkresult) -> {
-- return (ChunkAccess) ChunkResult.orElse(chunkresult, ichunkaccess);
-+ private void broadcast(List<ServerPlayer> players, Packet<?> packet) {
-+ players.forEach((entityplayer) -> {
-+ entityplayer.connection.send(packet);
- });
+ @Override
+ public int getQueueLevel() {
+- return this.queueLevel;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
-- public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
-- if (this.sendSync.isDone()) {
-- this.sendSync = postProcessingFuture;
-- } else {
-- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
-- return null;
-- });
-- }
--
-- }
-+ // Paper - rewrite chunk system
-
- public FullChunkStatus getFullStatus() {
-- return ChunkLevel.fullStatus(this.ticketLevel);
-+ return this.newChunkHolder.getChunkStatus(); // Paper - rewrite chunk system
+ private void setQueueLevel(int level) {
+- this.queueLevel = level;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- public final ChunkPos getPos() { // Paper - final for inline
-@@ -419,238 +365,17 @@ public class ChunkHolder {
+ public void setTicketLevel(int level) {
+- this.ticketLevel = level;
++ // Paper - rewrite chunk system
}
- public final int getTicketLevel() { // Paper - final for inline
-- return this.ticketLevel;
-- }
--
-- public int getQueueLevel() {
-- return this.queueLevel;
-- }
--
-- private void setQueueLevel(int level) {
-- this.queueLevel = level;
-- }
--
-- public void setTicketLevel(int level) {
-- this.ticketLevel = level;
-- }
--
-- private void scheduleFullChunkPromotion(ChunkMap playerchunkmap, CompletableFuture<ChunkResult<LevelChunk>> completablefuture, Executor executor, FullChunkStatus fullchunkstatus) {
+ private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
- CompletableFuture<Void> completablefuture1 = new CompletableFuture();
-
- completablefuture1.thenRunAsync(() -> {
-- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus);
+- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
- }, executor);
- this.pendingFullStateConfirmation = completablefuture1;
-- completablefuture.thenAccept((chunkresult) -> {
+- chunkFuture.thenAccept((chunkresult) -> {
- chunkresult.ifSuccess((chunk) -> {
- completablefuture1.complete(null); // CraftBukkit - decompile error
- });
- });
-- }
--
-- private void demoteFullChunk(ChunkMap playerchunkmap, FullChunkStatus fullchunkstatus) {
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
-- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus);
-- }
--
-- protected void updateFutures(ChunkMap chunkStorage, Executor executor) {
-- ChunkStatus chunkstatus = ChunkLevel.generationStatus(this.oldTicketLevel);
-- ChunkStatus chunkstatus1 = ChunkLevel.generationStatus(this.ticketLevel);
-- boolean flag = ChunkLevel.isLoaded(this.oldTicketLevel);
-- boolean flag1 = ChunkLevel.isLoaded(this.ticketLevel);
+- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
+- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
+- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
- // CraftBukkit start
- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
-- if (fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && !fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
-- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
+- if (flag && !flag1) {
+- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
-- chunkStorage.callbackExecutor.execute(() -> {
+- chunkLoadingManager.callbackExecutor.execute(() -> {
- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
- // These actions may however happen deferred, so we manually set the needsSaving flag already here.
@@ -16290,32 +22825,15 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- });
-
- // Run callback right away if the future was already done
-- chunkStorage.callbackExecutor.run();
+- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
-
-- if (flag) {
-- ChunkResult<ChunkAccess> chunkresult = ChunkResult.error(() -> {
-- return "Unloaded ticket level " + String.valueOf(this.pos);
-- });
--
-- for (int i = flag1 ? chunkstatus1.getIndex() + 1 : 0; i <= chunkstatus.getIndex(); ++i) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = (CompletableFuture) this.futures.get(i);
--
-- if (completablefuture == null) {
-- this.futures.set(i, CompletableFuture.completedFuture(chunkresult));
-- }
-- }
-- }
--
-- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
-- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
--
-- this.wasAccessibleSinceLastSave |= flag3;
-- if (!flag2 && flag3) {
+- this.wasAccessibleSinceLastSave |= flag1;
+- if (!flag && flag1) {
- int expectCreateCount = ++this.fullChunkCreateCount; // Paper
-- this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this);
-- this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, FullChunkStatus.FULL);
+- this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
+- this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
- // Paper start - cache ticking ready status
- this.fullChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
@@ -16325,10 +22843,11 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- }
- });
- });
-- this.updateChunkToSave(this.fullChunkFuture, "full");
+- // Paper end - cache ticking ready status
+- this.addSaveDependency(this.fullChunkFuture);
- }
-
-- if (flag2 && !flag3) {
+- if (flag && !flag1) {
- // Paper start
- if (this.isFullChunkReady) {
- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
@@ -16336,16 +22855,14 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- // Paper end
- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- ++this.fullChunkCreateCount; // Paper - cache ticking ready status
-- this.isFullChunkReady = false; // Paper - cache ticking ready status
- }
-
-- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
-- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
+- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
+- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
-
-- if (!flag4 && flag5) {
-- this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this);
-- this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
+- if (!flag2 && flag3) {
+- this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
+- this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
- // Paper start - cache ticking ready status
- this.tickingChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
@@ -16355,10 +22872,10 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- });
- });
- // Paper end
-- this.updateChunkToSave(this.tickingChunkFuture, "ticking");
+- this.addSaveDependency(this.tickingChunkFuture);
- }
-
-- if (flag4 && !flag5) {
+- if (flag2 && !flag3) {
- // Paper start
- if (this.isTickingReady) {
- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
@@ -16368,16 +22885,16 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- }
-
-- boolean flag6 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
-- boolean flag7 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
+- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
+- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
-
-- if (!flag6 && flag7) {
+- if (!flag4 && flag5) {
- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException());
- }
-
-- this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this);
-- this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
+- this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
+- this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
- // Paper start - cache ticking ready status
- this.entityTickingChunkFuture.thenAccept(chunkResult -> {
- chunkResult.ifSuccess(chunk -> {
@@ -16386,10 +22903,10 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- });
- });
- // Paper end
-- this.updateChunkToSave(this.entityTickingChunkFuture, "entity ticking");
+- this.addSaveDependency(this.entityTickingChunkFuture);
- }
-
-- if (flag6 && !flag7) {
+- if (flag4 && !flag5) {
- // Paper start
- if (this.isEntityTickingReady) {
- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
@@ -16400,7 +22917,7 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- }
-
- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) {
-- this.demoteFullChunk(chunkStorage, fullchunkstatus1);
+- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1);
- }
-
- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
@@ -16408,10 +22925,10 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- // CraftBukkit start
- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
-- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
+- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
-- chunkStorage.callbackExecutor.execute(() -> {
+- chunkLoadingManager.callbackExecutor.execute(() -> {
- chunk.loadCallback();
- });
- }
@@ -16422,71 +22939,61 @@ index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbb
- });
-
- // Run callback right away if the future was already done
-- chunkStorage.callbackExecutor.run();
+- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
-- }
--
-- public boolean wasAccessibleSinceLastSave() {
-- return this.wasAccessibleSinceLastSave;
-+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
-- public void refreshAccessibility() {
-- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL);
-- }
-+ // Paper - rewrite chunk system
-
- public void replaceProtoChunk(ImposterProtoChunk chunk) {
-- for (int i = 0; i < this.futures.length(); ++i) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = (CompletableFuture) this.futures.get(i);
--
-- if (completablefuture != null) {
-- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null);
--
-- if (ichunkaccess instanceof ProtoChunk) {
-- this.futures.set(i, CompletableFuture.completedFuture(ChunkResult.of(chunk)));
-- }
-- }
-- }
--
-- this.updateChunkToSave(CompletableFuture.completedFuture(ChunkResult.of(chunk.getWrapped())), "replaceProto");
+ public boolean wasAccessibleSinceLastSave() {
+- return this.wasAccessibleSinceLastSave;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
-- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList();
--
-- for (int i = 0; i < ChunkHolder.CHUNK_STATUSES.size(); ++i) {
-- list.add(Pair.of((ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i), (CompletableFuture) this.futures.get(i)));
-- }
--
-- return list;
+ public void refreshAccessibility() {
+- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@FunctionalInterface
-@@ -670,15 +395,15 @@ public class ChunkHolder {
+@@ -509,15 +428,15 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper start
public final boolean isEntityTickingReady() {
- return this.isEntityTickingReady;
-+ return this.newChunkHolder.isEntityTickingReady(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final boolean isTickingReady() {
- return this.isTickingReady;
-+ return this.newChunkHolder.isTickingReady(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final boolean isFullChunkReady() {
- return this.isFullChunkReady;
-+ return this.newChunkHolder.isFullChunkReady(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
// Paper end
}
+diff --git a/src/main/java/net/minecraft/server/level/ChunkLevel.java b/src/main/java/net/minecraft/server/level/ChunkLevel.java
+index d9ad32acdf46a43a649334a3b736aeb7b3af21d1..fae17a075d7efaf24d916877dd5968eb9652bb66 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkLevel.java
++++ b/src/main/java/net/minecraft/server/level/ChunkLevel.java
+@@ -7,9 +7,9 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
+ import org.jetbrains.annotations.Contract;
+
+ public class ChunkLevel {
+- private static final int FULL_CHUNK_LEVEL = 33;
+- private static final int BLOCK_TICKING_LEVEL = 32;
+- private static final int ENTITY_TICKING_LEVEL = 31;
++ public static final int FULL_CHUNK_LEVEL = 33;
++ public static final int BLOCK_TICKING_LEVEL = 32;
++ public static final int ENTITY_TICKING_LEVEL = 31;
+ private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+ public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
+ public static final int MAX_LEVEL = 33 + RADIUS_AROUND_FULL_CHUNK;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5 100644
+index 2ce7da9707d7c1a48b5609ae51a516d599d7aee8..b849e0cf15f894aa87b1bb397d85b887b8fb816e 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@@ -16496,17 +23003,14 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
-- private final LongSet entitiesInLevel;
+- private final List<ChunkGenerationTask> pendingGenerationTasks;
+ // Paper - rewrite chunk system
public final ServerLevel level;
private final ThreadedLevelLightEngine lightEngine;
- public final BlockableEventLoop<Runnable> mainThreadExecutor; // Paper - public
-@@ -134,15 +131,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private final ChunkGeneratorStructureState chunkGeneratorState;
- public final Supplier<DimensionDataStorage> overworldDataStorage;
+ private final BlockableEventLoop<Runnable> mainThreadExecutor;
+@@ -135,21 +132,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
private final PoiManager poiManager;
-- public final LongSet toDrop;
-+ // Paper - rewrite chunk system
+ public final LongSet toDrop;
private boolean modified;
- private final ChunkTaskPriorityQueueSorter queueSorter;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> worldgenMailbox;
@@ -16520,61 +23024,17 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
private final String storageName;
private final PlayerMap playerMap;
public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
-@@ -150,28 +145,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ private final Long2ByteMap chunkTypeCache;
private final Long2LongMap chunkSaveCooldowns;
- private final Queue<Runnable> unloadQueue;
+- private final Queue<Runnable> unloadQueue;
++ // Paper - rewrite chunk system
public int serverViewDistance;
-- private WorldGenContext worldGenContext;
--
-- // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
-- public final CallbackExecutor callbackExecutor = new CallbackExecutor();
-- public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
--
-- private final java.util.Queue<Runnable> queue = new java.util.ArrayDeque<>();
--
-- @Override
-- public void execute(Runnable runnable) {
-- this.queue.add(runnable);
-- }
--
-- @Override
-- public void run() {
-- Runnable task;
-- while ((task = this.queue.poll()) != null) {
-- task.run();
-- }
-- }
-- };
-- // CraftBukkit end
-+ private WorldGenContext worldGenContext; public final WorldGenContext getWorldGenContext() { return this.worldGenContext; } // Paper - rewrite chunk system
-
- // Paper start - distance maps
- private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
-@@ -181,6 +155,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
- // Note: players need to be explicitly added to distance maps before they can be updated
- this.nearbyPlayers.addPlayer(player);
-+ this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader
- }
-
- void removePlayerFromDistanceMaps(ServerPlayer player) {
-@@ -188,6 +163,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
- // Note: players need to be explicitly added to distance maps before they can be updated
- this.nearbyPlayers.removePlayer(player);
-+ this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader
- }
-
- void updateMaps(ServerPlayer player) {
-@@ -195,6 +171,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
- // Note: players need to be explicitly added to distance maps before they can be updated
- this.nearbyPlayers.tickPlayer(player);
-+ this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader
- }
- // Paper end
- // Paper start
-@@ -224,17 +201,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+- private final WorldGenContext worldGenContext;
++ public final WorldGenContext worldGenContext; // Paper - public
+
+ // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
+ public final CallbackExecutor callbackExecutor = new CallbackExecutor();
+@@ -223,23 +218,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
@@ -16588,45 +23048,44 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
- this.visibleChunkMap = this.updatingChunkMap.clone();
- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
-- this.entitiesInLevel = new LongOpenHashSet();
-- this.toDrop = new LongOpenHashSet();
+- this.pendingGenerationTasks = new ArrayList();
+ // Paper - rewrite chunk system
+ this.toDrop = new LongOpenHashSet();
this.tickingGenerated = new AtomicInteger();
this.playerMap = new PlayerMap();
this.entityMap = new Int2ObjectOpenHashMap();
-@@ -263,19 +237,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- this.chunkGeneratorState = chunkGenerator.createState(iregistrycustom.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, j, world.spigotConfig); // Spigot
- this.mainThreadExecutor = mainThreadExecutor;
-- ProcessorMailbox<Runnable> threadedmailbox = ProcessorMailbox.create(executor, "worldgen");
-+ // Paper - rewrite chunk system
-
- Objects.requireNonNull(mainThreadExecutor);
-- ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("main", mainThreadExecutor::tell);
+ this.chunkTypeCache = new Long2ByteOpenHashMap();
+ this.chunkSaveCooldowns = new Long2LongOpenHashMap();
+- this.unloadQueue = Queues.newConcurrentLinkedQueue();
+ // Paper - rewrite chunk system
+ Path path = session.getDimensionPath(world.dimension());
- this.progressListener = worldGenerationProgressListener;
+ this.storageName = path.getFileName().toString();
+@@ -270,15 +263,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.chunkStatusListener = chunkStatusChangeListener;
-- ProcessorMailbox<Runnable> threadedmailbox1 = ProcessorMailbox.create(executor, "light");
-+ // Paper - rewrite chunk system
+ ProcessorMailbox<Runnable> threadedmailbox1 = ProcessorMailbox.create(executor, "light");
- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE);
- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false);
- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false);
- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false));
+ // Paper - rewrite chunk system
-+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system
++ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, null); // Paper - rewrite chunk system
this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
this.overworldDataStorage = persistentStateManagerFactory;
- this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world);
-@@ -333,23 +305,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
+ this.setServerViewDistance(viewDistance);
+- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
++ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null); // Paper - rewrite chunk system
+ // Paper start
+ this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
+ this.regionManagers.add(this.dataRegionManager);
+@@ -319,23 +310,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
-+ // Paper start - rewrite player chunk loader
-+ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ);
-+ // Paper end - rewrite player chunk loader
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system
}
private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
@@ -16643,19 +23102,17 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
-
- return false;
- }
-+ // Paper start - rewrite player chunk loader
-+ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ, true);
-+ // Paper end - rewrite player chunk loader
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system
}
protected ThreadedLevelLightEngine getLightEngine() {
-@@ -358,20 +322,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -344,20 +323,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Nullable
protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
- return (ChunkHolder) this.updatingChunkMap.get(pos);
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
@@ -16664,7 +23121,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
public ChunkHolder getVisibleChunkIfPresent(long pos) {
- return (ChunkHolder) this.visibleChunkMap.get(pos);
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
@@ -16679,53 +23136,41 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
}
public String getChunkDebugData(ChunkPos chunkPos) {
-@@ -400,80 +366,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -386,55 +367,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
- if (margin == 0) {
- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0);
-
-- return centerChunk.getOrScheduleFuture(chunkstatus, this).thenApply((chunkresult) -> {
+- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> {
- return chunkresult.map(List::of);
- });
- } else {
- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList();
-- List<ChunkHolder> list1 = new ArrayList();
- ChunkPos chunkcoordintpair = centerChunk.getPos();
-- int j = chunkcoordintpair.x;
-- int k = chunkcoordintpair.z;
-
-- for (int l = -margin; l <= margin; ++l) {
-- for (int i1 = -margin; i1 <= margin; ++i1) {
-- int j1 = Math.max(Math.abs(i1), Math.abs(l));
-- ChunkPos chunkcoordintpair1 = new ChunkPos(j + i1, k + l);
-- long k1 = chunkcoordintpair1.toLong();
-- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(k1);
+- for (int j = -margin; j <= margin; ++j) {
+- for (int k = -margin; k <= margin; ++k) {
+- int l = Math.max(Math.abs(k), Math.abs(j));
+- long i1 = ChunkPos.asLong(chunkcoordintpair.x + k, chunkcoordintpair.z + j);
+- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(i1);
-
- if (playerchunk1 == null) {
-- return CompletableFuture.completedFuture(ChunkResult.error(() -> {
-- return "Unloaded " + String.valueOf(chunkcoordintpair1);
-- }));
+- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE;
- }
-
-- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(j1);
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = playerchunk1.getOrScheduleFuture(chunkstatus1, this);
+- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(l);
-
-- list1.add(playerchunk1);
-- list.add(completablefuture);
+- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this));
- }
- }
-
-- CompletableFuture<List<ChunkResult<ChunkAccess>>> completablefuture1 = Util.sequence(list);
-- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture2 = completablefuture1.thenApply((list2) -> {
-- List<ChunkAccess> list3 = Lists.newArrayList();
-- // CraftBukkit start - decompile error
-- int cnt = 0;
+- return Util.sequence(list).thenApply((list1) -> {
+- List<ChunkAccess> list2 = Lists.newArrayList();
+- Iterator iterator = list1.iterator();
-
-- for (Iterator iterator = list2.iterator(); iterator.hasNext(); ++cnt) {
-- final int l1 = cnt;
-- // CraftBukkit end
+- while (iterator.hasNext()) {
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next();
-
- if (chunkresult == null) {
@@ -16735,37 +23180,24 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
-
- if (ichunkaccess == null) {
-- return ChunkResult.error(() -> {
-- String s = String.valueOf(new ChunkPos(j + l1 % (margin * 2 + 1), k + l1 / (margin * 2 + 1)));
--
-- return "Unloaded " + s + " " + chunkresult.getError();
-- });
+- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
- }
-
-- list3.add(ichunkaccess);
+- list2.add(ichunkaccess);
- }
-
-- return ChunkResult.of(list3);
+- return ChunkResult.of(list2);
- });
-- Iterator iterator = list1.iterator();
--
-- while (iterator.hasNext()) {
-- ChunkHolder playerchunk2 = (ChunkHolder) iterator.next();
--
-- playerchunk2.addSaveDependency("getChunkRangeFuture " + String.valueOf(chunkcoordintpair) + " " + margin, completablefuture2);
-- }
--
-- return completablefuture2;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
-@@ -503,263 +396,72 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -464,93 +397,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
- public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder chunk) {
-- return this.getChunkRangeFuture(chunk, 2, (i) -> {
+ public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) {
+- return this.getChunkRangeFuture(holder, 2, (i) -> {
- return ChunkStatus.FULL;
- }).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
@@ -16823,14 +23255,9 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- } finally {
- super.close();
- }
+-
+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
-+ }
-
-+ // Paper start - rewrite chunk system
-+ protected void saveIncrementally() {
-+ this.level.chunkTaskScheduler.chunkHolderManager.autoSave(); // Paper - rewrite chunk system
}
-+ // Paper end - - rewrite chunk system
protected void saveAllChunks(boolean flush) {
- if (flush) {
@@ -16840,17 +23267,11 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- do {
- mutableboolean.setFalse();
- list.stream().map((playerchunk) -> {
-- CompletableFuture completablefuture;
--
-- do {
-- completablefuture = playerchunk.getChunkToSave();
-- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
+- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
-
-- Objects.requireNonNull(completablefuture);
-- iasynctaskhandler.managedBlock(completablefuture::isDone);
-- } while (completablefuture != playerchunk.getChunkToSave());
--
-- return (ChunkAccess) completablefuture.join();
+- Objects.requireNonNull(playerchunk);
+- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving);
+- return playerchunk.getLatestChunk();
- }).filter((ichunkaccess) -> {
- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
- }).filter(this::save).forEach((ichunkaccess) -> {
@@ -16866,24 +23287,13 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
- }
-
-+ this.level.chunkTaskScheduler.chunkHolderManager.saveAllChunks(flush, false, false); // Paper - rewrite chunk system
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks(
++ flush, false, false
++ );
}
protected void tick(BooleanSupplier shouldKeepTicking) {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
-+ try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper
- gameprofilerfiller.push("poi");
- this.poiManager.tick(shouldKeepTicking);
-+ } // Paper
- gameprofilerfiller.popPush("chunk_unload");
- if (!this.level.noSave()) {
-+ try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper
- this.processUnloads(shouldKeepTicking);
-+ } // Paper
- }
-
- gameprofilerfiller.pop();
+@@ -567,134 +430,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
@@ -16893,18 +23303,26 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
private void processUnloads(BooleanSupplier shouldKeepTicking) {
- LongIterator longiterator = this.toDrop.iterator();
+- int i = 0;
-
-- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
+- while (longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000)) {
- long j = longiterator.nextLong();
-- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
+- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(j);
-
- if (playerchunk != null) {
+- if (playerchunk.getGenerationRefCount() != 0) {
+- continue;
+- }
+-
+- this.updatingChunkMap.remove(j);
- playerchunk.onChunkRemove(); // Paper
- this.pendingUnloads.put(j, playerchunk);
- this.modified = true;
- ++i;
- this.scheduleUnload(j, playerchunk);
- }
+-
+- longiterator.remove();
- }
-
- int k = Math.max(0, this.unloadQueue.size() - 2000);
@@ -16924,31 +23342,34 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- ++l;
- }
- }
-+ this.level.chunkTaskScheduler.chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system
}
private void scheduleUnload(long pos, ChunkHolder holder) {
-- CompletableFuture<ChunkAccess> completablefuture = holder.getChunkToSave();
-- Consumer<ChunkAccess> consumer = (ichunkaccess) -> { // CraftBukkit - decompile error
-- CompletableFuture<ChunkAccess> completablefuture1 = holder.getChunkToSave();
--
-- if (completablefuture1 != completablefuture) {
+- CompletableFuture completablefuture = holder.getSaveSyncFuture();
+- Runnable runnable = () -> {
+- if (!holder.isReadyForSaving()) {
- this.scheduleUnload(pos, holder);
- } else {
+- ChunkAccess ichunkaccess = holder.getLatestChunk();
+-
- // Paper start
- boolean removed;
- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
- // Paper end
+- LevelChunk chunk;
+-
- if (ichunkaccess instanceof LevelChunk) {
-- ((LevelChunk) ichunkaccess).setLoaded(false);
+- chunk = (LevelChunk) ichunkaccess;
+- chunk.setLoaded(false);
- }
-
- this.save(ichunkaccess);
-- if (this.entitiesInLevel.remove(pos) && ichunkaccess instanceof LevelChunk) {
-- LevelChunk chunk = (LevelChunk) ichunkaccess;
--
+- if (ichunkaccess instanceof LevelChunk) {
+- chunk = (LevelChunk) ichunkaccess;
- this.level.unload(chunk);
- }
-
@@ -16965,7 +23386,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- Queue queue = this.unloadQueue;
-
- Objects.requireNonNull(this.unloadQueue);
-- completablefuture.thenAcceptAsync(consumer, queue::add).whenComplete((ovoid, throwable) -> {
+- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> {
- if (throwable != null) {
- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable);
- }
@@ -16985,34 +23406,6 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- public CompletableFuture<ChunkResult<ChunkAccess>> schedule(ChunkHolder holder, ChunkStatus requiredStatus) {
-- ChunkPos chunkcoordintpair = holder.getPos();
--
-- if (requiredStatus == ChunkStatus.EMPTY) {
-- return this.scheduleChunkLoad(chunkcoordintpair).thenApply(ChunkResult::of);
-- } else {
-- if (requiredStatus == ChunkStatus.LIGHT) {
-- this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, ChunkLevel.byStatus(ChunkStatus.LIGHT), chunkcoordintpair);
-- }
--
-- if (!requiredStatus.hasLoadDependencies()) {
-- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null);
--
-- if (ichunkaccess != null && ichunkaccess.getStatus().isOrAfter(requiredStatus)) {
-- CompletableFuture<ChunkAccess> completablefuture = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> {
-- return this.protoChunkToFullChunk(holder, ichunkaccess1);
-- }, ichunkaccess);
--
-- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus);
-- return completablefuture.thenApply(ChunkResult::of);
-- }
-- }
--
-- return this.scheduleChunkGeneration(holder, requiredStatus);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
- return this.readChunk(pos).thenApply((optional) -> {
- return optional.filter((nbttagcompound) -> {
@@ -17027,9 +23420,9 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- }).thenApplyAsync((optional) -> {
- this.level.getProfiler().incrementCounter("chunkLoad");
- if (optional.isPresent()) {
-- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, pos, (CompoundTag) optional.get());
+- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, this.storageInfo(), pos, (CompoundTag) optional.get());
-
-- this.markPosition(pos, protochunk.getStatus().getChunkType());
+- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType());
- return protochunk;
- } else {
- return this.createEmptyChunk(pos);
@@ -17040,141 +23433,91 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
-- private static boolean isChunkDataValid(CompoundTag nbt) {
-+ public static boolean isChunkDataValid(CompoundTag nbt) { // Paper - async chunk loading
- return nbt.contains("Status", 8);
+ private static boolean isChunkDataValid(CompoundTag nbt) {
+@@ -754,137 +508,44 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ @Override
+ public GenerationChunkHolder acquireGeneration(long pos) {
+- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos);
+-
+- playerchunk.increaseGenerationRefCount();
+- return playerchunk;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
-@@ -816,60 +518,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ @Override
+ public void releaseGeneration(GenerationChunkHolder chunkHolder) {
+- chunkHolder.decreaseGenerationRefCount();
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- private CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGeneration(ChunkHolder holder, ChunkStatus requiredStatus) {
-- ChunkPos chunkcoordintpair = holder.getPos();
-- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, requiredStatus.getRange(), (i) -> {
-- return this.getDependencyStatus(requiredStatus, i);
-- });
--
-- this.level.getProfiler().incrementCounter(() -> {
-- return "chunkGenerate " + String.valueOf(requiredStatus);
-- });
-- Executor executor = (runnable) -> {
-- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
-- };
--
-- return completablefuture.thenComposeAsync((chunkresult) -> {
-- List<ChunkAccess> list = (List) chunkresult.orElse(null); // CraftBukkit - decompile error
+ @Override
+ public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) {
+- ChunkPos chunkcoordintpair = chunkHolder.getPos();
-
-- if (list == null) {
-- this.releaseLightTicket(chunkcoordintpair);
-- Objects.requireNonNull(chunkresult);
-- return CompletableFuture.completedFuture(ChunkResult.error(chunkresult::getError));
-- } else {
-- try {
-- ChunkAccess ichunkaccess = (ChunkAccess) list.get(list.size() / 2);
-- CompletableFuture completablefuture1;
+- if (step.targetStatus() == ChunkStatus.EMPTY) {
+- return this.scheduleChunkLoad(chunkcoordintpair);
+- } else {
+- try {
+- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z);
+- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent());
-
-- if (ichunkaccess.getStatus().isOrAfter(requiredStatus)) {
-- completablefuture1 = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> {
-- return this.protoChunkToFullChunk(holder, ichunkaccess1);
-- }, ichunkaccess);
-- } else {
-- completablefuture1 = requiredStatus.generate(this.worldGenContext, executor, (ichunkaccess1) -> {
-- return this.protoChunkToFullChunk(holder, ichunkaccess1);
-- }, list);
-- }
+- if (ichunkaccess == null) {
+- throw new IllegalStateException("Parent chunk missing");
+- } else {
+- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess);
-
-- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus);
-- return completablefuture1.thenApply(ChunkResult::of);
-- } catch (Exception exception) {
-- exception.getStackTrace();
-- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
-- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
+- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus());
+- return completablefuture;
+- }
+- } catch (Exception exception) {
+- exception.getStackTrace();
+- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
+- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
-
-- crashreportsystemdetails.setDetail("Status being generated", () -> {
-- return BuiltInRegistries.CHUNK_STATUS.getKey(requiredStatus).toString();
-- });
-- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
-- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
-- crashreportsystemdetails.setDetail("Generator", (Object) this.generator);
-- this.mainThreadExecutor.execute(() -> {
-- throw new ReportedException(crashreport);
-- });
+- crashreportsystemdetails.setDetail("Status being generated", () -> {
+- return step.targetStatus().getName();
+- });
+- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
+- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
+- crashreportsystemdetails.setDetail("Generator", (Object) this.generator());
+- this.mainThreadExecutor.execute(() -> {
- throw new ReportedException(crashreport);
-- }
+- });
+- throw new ReportedException(crashreport);
- }
-- }, executor);
+- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- protected void releaseLightTicket(ChunkPos pos) {
-@@ -880,7 +529,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }));
- }
-
-- private ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) {
-+ public static ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { // Paper -> public, static
- ChunkStatus chunkstatus1;
-
- if (distance == 0) {
-@@ -892,7 +541,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- return chunkstatus1;
- }
-
-- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> nbt) {
-+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> nbt, ChunkPos position) { // Paper - public and add chunk position parameter
- if (!nbt.isEmpty()) {
- // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
- world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world).filter((entity) -> {
-@@ -908,45 +557,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
- return !needsRemoval;
-- }));
-+ }), position); // Paper - rewrite chunk system
- // CraftBukkit end
- }
-
+ @Override
+ public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) {
+- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos);
+-
+- this.pendingGenerationTasks.add(chunkgenerationtask);
+- return chunkgenerationtask;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- private CompletableFuture<ChunkAccess> protoChunkToFullChunk(ChunkHolder playerchunk, ChunkAccess ichunkaccess) {
-- return CompletableFuture.supplyAsync(() -> {
-- ChunkPos chunkcoordintpair = playerchunk.getPos();
-- ProtoChunk protochunk = (ProtoChunk) ichunkaccess;
-- LevelChunk chunk;
+ private void runGenerationTask(ChunkGenerationTask chunkLoader) {
+- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkLoader.getCenter(), () -> {
+- CompletableFuture<?> completablefuture = chunkLoader.runUntilWait();
-
-- if (protochunk instanceof ImposterProtoChunk) {
-- chunk = ((ImposterProtoChunk) protochunk).getWrapped();
-- } else {
-- chunk = new LevelChunk(this.level, protochunk, (chunk1) -> {
-- ChunkMap.postLoadProtoChunk(this.level, protochunk.getEntities());
+- if (completablefuture != null) {
+- completablefuture.thenRun(() -> {
+- this.runGenerationTask(chunkLoader);
- });
-- playerchunk.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
- }
--
-- chunk.setFullStatus(() -> {
-- return ChunkLevel.fullStatus(playerchunk.getTicketLevel());
-- });
-- chunk.runPostLoad();
-- if (this.entitiesInLevel.add(chunkcoordintpair.toLong())) {
-- chunk.setLoaded(true);
-- chunk.registerAllBlockEntitiesAfterLevelLoad();
-- chunk.registerTickContainerInLevel(this.level);
-- }
--
-- return chunk;
-- }, (runnable) -> {
-- ProcessorHandle mailbox = this.mainThreadMailbox;
-- long i = playerchunk.getPos().toLong();
--
-- Objects.requireNonNull(playerchunk);
-- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, playerchunk::getTicketLevel));
-- });
+- }));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
- // Paper start - duplicate uuid resolving
-@@ -990,61 +608,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- // Paper end - duplicate uuid resolving
+ @Override
+ public void runGenerationTasks() {
+- this.pendingGenerationTasks.forEach(this::runGenerationTask);
+- this.pendingGenerationTasks.clear();
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> {
@@ -17190,7 +23533,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- return chunkresult.ifSuccess((chunk) -> {
- chunk.postProcessGeneration();
- this.level.startTickingChunk(chunk);
-- CompletableFuture<?> completablefuture2 = holder.getChunkSendSyncFuture();
+- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture();
-
- if (completablefuture2.isDone()) {
- this.onChunkReadyToSend(chunk);
@@ -17222,12 +23565,12 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- ChunkMap.markChunkPendingToSend(entityplayer, chunk);
- }
- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) {
-- return this.getChunkRangeFuture(holder, 1, ChunkStatus::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> {
+- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
@@ -17238,14 +23581,12 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
}
public int getTickingGenerated() {
-@@ -1052,96 +625,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -892,135 +553,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) {
-- if (!chunkHolder.wasAccessibleSinceLastSave()) {
-- return false;
-- } else {
-- ChunkAccess ichunkaccess = (ChunkAccess) chunkHolder.getChunkToSave().getNow(null); // CraftBukkit - decompile error
+- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) {
+- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk();
-
- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) {
- return false;
@@ -17267,6 +23608,8 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- return flag;
- }
- }
+- } else {
+- return false;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@@ -17280,7 +23623,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- ChunkPos chunkcoordintpair = chunk.getPos();
-
- try {
-- ChunkStatus chunkstatus = chunk.getStatus();
+- ChunkStatus chunkstatus = chunk.getPersistedStatus();
-
- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) {
- if (this.isExistingChunkFull(chunkcoordintpair)) {
@@ -17295,15 +23638,14 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- this.level.getProfiler().incrementCounter("chunkSave");
- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk);
-
-- this.write(chunkcoordintpair, nbttagcompound).exceptionallyAsync((throwable) -> {
-- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair);
+- this.write(chunkcoordintpair, nbttagcompound).exceptionally((throwable) -> {
+- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair);
- return null;
-- }, this.mainThreadExecutor);
+- });
- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
- return true;
- } catch (Exception exception) {
-- ChunkMap.LOGGER.error("Failed to save chunk {},{}", new Object[]{chunkcoordintpair.x, chunkcoordintpair.z, exception});
-- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair);
+- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair);
- return false;
- }
- }
@@ -17338,35 +23680,32 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
}
public void setServerViewDistance(int watchDistance) { // Paper - public
-@@ -1149,37 +641,36 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- if (j != this.serverViewDistance) {
- this.serverViewDistance = j;
+- int j = Mth.clamp(watchDistance, 2, 32);
+-
+- if (j != this.serverViewDistance) {
+- this.serverViewDistance = j;
- this.distanceManager.updatePlayerTickets(this.serverViewDistance);
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-+ this.level.playerChunkLoader.setLoadDistance(this.serverViewDistance + 1); // Paper - replace player loader system
-+ }
-
+-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+ }
-
+-
- this.updateChunkTracking(entityplayer);
- }
-- }
-+ // Paper start - replace player loader system
-+ public void setTickViewDistance(int distance) {
-+ this.level.playerChunkLoader.setTickDistance(distance);
-+ }
++ // Paper start - rewrite chunk system
++ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
++ if (clamped == this.serverViewDistance) {
++ return;
+ }
-+ public void setSendViewDistance(int distance) {
-+ this.level.playerChunkLoader.setSendDistance(distance);
++ this.serverViewDistance = clamped;
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1);
++ // Paper end - rewrite chunk system
}
-+ // Paper end - replace player loader system
public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
-+ return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player); // Paper - per player view distance
++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
}
private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
@@ -17375,114 +23714,98 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- if (chunk != null) {
- ChunkMap.markChunkPendingToSend(player, chunk);
- }
-+ throw new UnsupportedOperationException(); // Paper - per player view distance
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
- player.connection.chunkSender.markChunkPendingToSend(chunk);
-+ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void dropChunk(ServerPlayer player, ChunkPos pos) {
- player.connection.chunkSender.dropChunk(player, pos);
-+ // Paper - rewrite player chunk loader
- }
-
- @Nullable
-@@ -1202,30 +693,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- void dumpChunks(Writer writer) throws IOException {
-- CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
-- TickingTracker tickingtracker = this.distanceManager.tickingTracker();
-- Iterator<ChunkHolder> objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
--
-- while (objectbidirectionaliterator.hasNext()) {
-- ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
-- long i = playerchunk.pos.toLong(); // Paper
-- ChunkPos chunkcoordintpair = new ChunkPos(i);
-- // Paper
-- Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLastAvailable());
-- Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
-- return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
-- });
--
-- // CraftBukkit - decompile error
-- csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> {
-- return chunk.getBlockEntities().size();
-- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> {
-- return chunk.getBlockTicks().count();
-- }).orElse(0), optional1.map((chunk) -> {
-- return chunk.getFluidTicks().count();
-- }).orElse(0));
-- }
--
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private static String printFuture(CompletableFuture<ChunkResult<LevelChunk>> future) {
-@@ -1240,6 +708,32 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
++ // Paper - rewrite chunk system
++ }
++
++ // Paper start - rewrite chunk system
++ @Override
++ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) {
++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
++ try {
++ return CompletableFuture.completedFuture(
++ Optional.ofNullable(
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
++ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA,
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
++ )
++ )
++ );
++ } catch (final Throwable thr) {
++ return CompletableFuture.failedFuture(thr);
++ }
++ }
++ return super.read(pos);
}
-+ // Paper start - Asynchronous chunk io
-+ @Nullable
+ @Override
-+ public CompoundTag readSync(ChunkPos chunkcoordintpair) throws IOException {
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData(
-+ this.level, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
-+ );
++ public CompletableFuture<Void> write(final ChunkPos pos, final CompoundTag tag) {
++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(
++ this.level, pos.x, pos.z, tag,
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA);
++ return null;
+ }
-+ return super.readSync(chunkcoordintpair);
++ super.write(pos, tag);
++ return null;
+ }
+
+ @Override
-+ public CompletableFuture<Void> write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException {
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave(
-+ this.level, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA);
-+ return null;
-+ }
-+ super.write(chunkcoordintpair, nbttagcompound);
-+ return null;
++ public void flushWorker() {
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.flush();
+ }
-+ // Paper end
++ // Paper end - rewrite chunk system
+
- private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos chunkPos) {
- return this.read(chunkPos).thenApplyAsync((optional) -> {
- return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit
-@@ -1340,8 +834,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
+ @Nullable
+ public LevelChunk getChunkToSend(long pos) {
+ ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
+@@ -1086,7 +696,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+
+ // CraftBukkit start
+- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
++ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public
+ return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
+ // CraftBukkit end
+ }
+@@ -1180,7 +790,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
-- player.setChunkTrackingView(ChunkTrackingView.EMPTY);
+ player.setChunkTrackingView(ChunkTrackingView.EMPTY);
- this.updateChunkTracking(player);
-+ // Paper - handled by player chunk loader
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
this.addPlayerToDistanceMaps(player); // Paper - distance maps
} else {
SectionPos sectionposition = player.getLastSectionPos();
-@@ -1352,7 +845,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1191,7 +801,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
this.removePlayerFromDistanceMaps(player); // Paper - distance maps
- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
-+ // Paper - handled by player chunk loader
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
}
}
-@@ -1400,71 +893,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1239,71 +849,31 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.playerMap.unIgnorePlayer(player);
}
- this.updateChunkTracking(player);
-+ // Paper - replaced by PlayerChunkLoader
++ // Paper - rewrite chunk system
}
this.updateMaps(player); // Paper - distance maps
++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
}
private void updateChunkTracking(ServerPlayer player) {
@@ -17497,7 +23820,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- }
-
- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i));
-+ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) {
@@ -17528,7 +23851,7 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- });
- player.setChunkTrackingView(chunkFilter);
- }
-+ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
@@ -17543,20 +23866,20 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) {
- builder.add(entityplayer);
- }
-+ // Paper start - per player view distance
-+ ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
++ // Paper start - rewrite chunk system
++ final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
+ if (holder == null) {
-+ return new java.util.ArrayList<>();
++ return new ArrayList<>();
+ } else {
-+ return holder.getPlayers(onlyOnWatchDistanceEdge);
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge);
}
-
- return builder.build();
-+ // Paper end - per player view distance
++ // Paper end - rewrite chunk system
}
public void addEntity(Entity entity) {
-@@ -1535,13 +987,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1374,13 +944,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected void tick() {
@@ -17567,11 +23890,11 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
-
- this.updateChunkTracking(entityplayer);
- }
-+ // Paper - replaced by PlayerChunkLoader
++ // Paper - rewrite chunk system
List<ServerPlayer> list = Lists.newArrayList();
List<ServerPlayer> list1 = this.level.players();
-@@ -1648,16 +1094,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1487,27 +1051,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
@@ -17585,12 +23908,23 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
- }
-
- });
-+ // Paper - rewrite player chunk loader
++ // Paper - rewrite chunk system
}
- public class ChunkDistanceManager extends DistanceManager { // Paper - public
-@@ -1668,7 +1105,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+- public class ChunkDistanceManager extends DistanceManager { // Paper - public
++ public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
+
+ protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
+ super(workerExecutor, mainThreadExecutor, ChunkMap.this); // Paper
+ }
++ // Paper start - rewrite chunk system
++ @Override
++ public final ChunkMap moonrise$getChunkMap() {
++ return ChunkMap.this;
++ }
++ // Paper end - rewrite chunk system
++
@Override
protected boolean isChunkToRemove(long pos) {
- return ChunkMap.this.toDrop.contains(pos);
@@ -17599,19 +23933,16 @@ index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd1502
@Nullable
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831c038d2a6 100644
+index cbabbfbb9967ddf9a56f3be24a88e0fcd4415aa2..71abe25cfb73af3857cbc85980aa32d0201aab62 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -38,65 +38,28 @@ import org.slf4j.Logger;
+@@ -36,66 +36,36 @@ import net.minecraft.world.level.ChunkPos;
+ import net.minecraft.world.level.chunk.LevelChunk;
+ import org.slf4j.Logger;
- public abstract class DistanceManager {
+-public abstract class DistanceManager {
++public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - rewrite chunk system
-+ // Paper start - rewrite chunk system
-+ public io.papermc.paper.chunk.system.scheduling.ChunkHolderManager getChunkHolderManager() {
-+ return this.chunkMap.level.chunkTaskScheduler.chunkHolderManager;
-+ }
-+ // Paper end - rewrite chunk system
-+
static final Logger LOGGER = LogUtils.getLogger();
static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
@@ -17628,16 +23959,23 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
- final LongSet ticketsToRelease = new LongOpenHashSet();
- final Executor mainThreadExecutor;
-- private long ticketTickCounter;
++ // Paper - rewrite chunk system
+ private long ticketTickCounter;
- public int simulationDistance = 10;
+ // Paper - rewrite chunk system
private final ChunkMap chunkMap; // Paper
++ // Paper start - rewrite chunk system
++ public ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager getChunkHolderManager() {
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager;
++ }
++ // Paper end - rewrite chunk system
++
protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) {
-- Objects.requireNonNull(mainThreadExecutor);
-- ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute);
-- ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4);
--
+ Objects.requireNonNull(mainThreadExecutor);
+ ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute);
+ ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4);
+
- this.ticketThrottler = chunktaskqueuesorter;
- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true);
- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox);
@@ -17673,15 +24011,14 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- objectiterator.remove();
- }
- }
--
+ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system
+
}
- private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
-@@ -112,108 +75,25 @@ public abstract class DistanceManager {
+@@ -112,86 +82,15 @@ public abstract class DistanceManager {
protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
- public boolean runAllUpdates(ChunkMap chunkStorage) {
+ public boolean runAllUpdates(ChunkMap chunkLoadingManager) {
- this.naturalSpawnChunkCounter.runAllUpdates();
- this.tickingTicketsTracker.runAllUpdates();
- this.playerTicketManager.runAllUpdates();
@@ -17693,25 +24030,13 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- }
-
- if (!this.chunksToUpdateFutures.isEmpty()) {
-- // CraftBukkit start
-- // Iterate pending chunk updates with protection against concurrent modification exceptions
-- java.util.Iterator<ChunkHolder> iter = this.chunksToUpdateFutures.iterator();
-- int expectedSize = this.chunksToUpdateFutures.size();
-- do {
-- ChunkHolder playerchunk = iter.next();
-- iter.remove();
-- expectedSize--;
--
-- playerchunk.updateFutures(chunkStorage, this.mainThreadExecutor);
--
-- // Reset iterator if set was modified using add()
-- if (this.chunksToUpdateFutures.size() != expectedSize) {
-- expectedSize = this.chunksToUpdateFutures.size();
-- iter = this.chunksToUpdateFutures.iterator();
-- }
-- } while (iter.hasNext());
-- // CraftBukkit end
--
+- this.chunksToUpdateFutures.forEach((playerchunk) -> {
+- playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
+- });
+- this.chunksToUpdateFutures.forEach((playerchunk) -> {
+- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor);
+- });
+- this.chunksToUpdateFutures.clear();
- return true;
- } else {
- if (!this.ticketsToRelease.isEmpty()) {
@@ -17723,7 +24048,7 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- if (this.getTickets(j).stream().anyMatch((ticket) -> {
- return ticket.getType() == TicketType.PLAYER;
- })) {
-- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j);
+- ChunkHolder playerchunk = chunkLoadingManager.getUpdatingChunkIfPresent(j);
-
- if (playerchunk == null) {
- throw new IllegalStateException();
@@ -17759,7 +24084,6 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- }
-
- return ticket == ticket1; // CraftBukkit
-+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper
+ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
@@ -17777,24 +24101,11 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
-
- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
- return removed; // CraftBukkit
-+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper
+ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
-- this.addTicket(pos.toLong(), new Ticket<>(type, level, argument));
-+ this.getChunkHolderManager().addTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system
- }
-
- public <T> void removeTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
-- Ticket<T> ticket = new Ticket<>(type, level, argument);
--
-- this.removeTicket(pos.toLong(), ticket);
-+ this.getChunkHolderManager().removeTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system
- }
-
- public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
-@@ -222,13 +102,7 @@ public abstract class DistanceManager {
+@@ -210,13 +109,7 @@ public abstract class DistanceManager {
}
public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
@@ -17809,7 +24120,7 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
}
public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
-@@ -237,31 +111,21 @@ public abstract class DistanceManager {
+@@ -225,32 +118,21 @@ public abstract class DistanceManager {
}
public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
@@ -17823,76 +24134,71 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
+ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
}
-- private SortedArraySet<Ticket<?>> getTickets(long position) {
+ private SortedArraySet<Ticket<?>> getTickets(long position) {
- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> {
- return SortedArraySet.create(4);
- });
-- }
-+ // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
protected void updateChunkForced(ChunkPos pos, boolean forced) {
- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos);
-+ Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos, 0L); // Paper - rewrite chunk system
- long i = pos.toLong();
-
+- long i = pos.toLong();
+-
++ // Paper start - rewrite chunk system
if (forced) {
- this.addTicket(i, ticket);
+- this.addTicket(i, ticket);
- this.tickingTicketsTracker.addTicket(i, ticket);
-+ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used
++ this.getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
} else {
- this.removeTicket(i, ticket);
+- this.removeTicket(i, ticket);
- this.tickingTicketsTracker.removeTicket(i, ticket);
-+ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used
++ this.getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
}
++ // Paper end - rewrite chunk system
}
-@@ -270,12 +134,10 @@ public abstract class DistanceManager {
- ChunkPos chunkcoordintpair = pos.chunk();
- long i = chunkcoordintpair.toLong();
-- ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
-- return new ObjectOpenHashSet();
-- })).add(player);
-+ // Paper - no longer used
+@@ -262,8 +144,7 @@ public abstract class DistanceManager {
+ return new ObjectOpenHashSet();
+ })).add(player);
this.naturalSpawnChunkCounter.update(i, 0, true);
- this.playerTicketManager.update(i, 0, true);
- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
-+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used
-+ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
++ // Paper - rewrite chunk system
}
public void removePlayer(SectionPos pos, ServerPlayer player) {
-@@ -288,40 +150,44 @@ public abstract class DistanceManager {
+@@ -276,39 +157,39 @@ public abstract class DistanceManager {
if (objectset == null || objectset.isEmpty()) { // Paper
this.playersPerChunk.remove(i);
this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
- this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
-+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
-+ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
++ // Paper - rewrite chunk system
}
}
-- private int getPlayerTicketLevel() {
+ private int getPlayerTicketLevel() {
- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance);
-- }
-+ // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
public boolean inEntityTickingRange(long chunkPos) {
- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos));
-+ // Paper start - replace player chunk loader system
-+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos);
-+ return holder != null && holder.isEntityTickingReady();
-+ // Paper end - replace player chunk loader system
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
++ return chunkHolder != null && chunkHolder.isEntityTickingReady();
++ // Paper end - rewrite chunk system
}
public boolean inBlockTickingRange(long chunkPos) {
- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos));
-+ // Paper start - replace player chunk loader system
-+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos);
-+ return holder != null && holder.isTickingReady();
-+ // Paper end - replace player chunk loader system
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
++ return chunkHolder != null && chunkHolder.isTickingReady();
++ // Paper end - rewrite chunk system
}
protected String getTicketDebugString(long pos) {
@@ -17904,26 +24210,19 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
protected void updatePlayerTickets(int viewDistance) {
- this.playerTicketManager.updateViewDistance(viewDistance);
-+ this.chunkMap.setServerViewDistance(viewDistance); // Paper - route to player chunk manager
++ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system
}
-- public void updateSimulationDistance(int simulationDistance) {
+ public void updateSimulationDistance(int simulationDistance) {
- if (simulationDistance != this.simulationDistance) {
- this.simulationDistance = simulationDistance;
- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel());
- }
-+ // Paper start
-+ public int getSimulationDistance() {
-+ return this.chunkMap.level.playerChunkLoader.getAPITickDistance();
-+ }
-+ // Paper end
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(simulationDistance); // Paper - rewrite chunk system
-+ public void updateSimulationDistance(int simulationDistance) {
-+ this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager
}
- public int getNaturalSpawnChunkCount() {
-@@ -335,103 +201,26 @@ public abstract class DistanceManager {
+@@ -323,103 +204,35 @@ public abstract class DistanceManager {
}
public String getDebugStatus() {
@@ -17931,7 +24230,7 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
+ return "No DistanceManager stats available"; // Paper - rewrite chunk system
}
-- private void dumpTickets(String path) {
+ private void dumpTickets(String path) {
- try {
- FileOutputStream fileoutputstream = new FileOutputStream(new File(path));
-
@@ -17963,17 +24262,18 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- } catch (IOException ioexception) {
- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception);
- }
--
-- }
--
-- @VisibleForTesting
-- TickingTracker tickingTracker() {
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+
+ }
+
+ @VisibleForTesting
+ TickingTracker tickingTracker() {
- return this.tickingTicketsTracker;
-- }
-+ // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
public void removeTicketsOnClosing() {
-- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve
+- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
@@ -17999,13 +24299,13 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
- objectiterator.remove();
- }
- }
--
-+ // Paper - rewrite chunk system - this stupid hack ain't needed anymore
++ // Paper - rewrite chunk system
+
}
public boolean hasTickets() {
- return !this.tickets.isEmpty();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
// CraftBukkit start
@@ -18029,67 +24329,425 @@ index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831
}
// CraftBukkit end
-+ /* Paper - rewrite chunk system
++ /* // Paper - rewrite chunk system
private class ChunkTicketTracker extends ChunkTracker {
private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
-@@ -478,6 +267,7 @@ public abstract class DistanceManager {
+@@ -465,7 +278,7 @@ public abstract class DistanceManager {
+ public int runDistanceUpdates(int distance) {
return this.runUpdates(distance);
}
- }
-+ */ // Paper - rewrite chunk system
+- }
++ }*/ // Paper - rewrite chunk system
private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
-@@ -557,6 +347,7 @@ public abstract class DistanceManager {
+@@ -545,6 +358,7 @@ public abstract class DistanceManager {
}
}
-+ /* Paper - rewrite chunk system
++ /* // Paper - rewrite chunk system
private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
private int viewDistance = 0;
-@@ -652,4 +443,5 @@ public abstract class DistanceManager {
+@@ -639,5 +453,5 @@ public abstract class DistanceManager {
+ private boolean haveTicketFor(int distance) {
return distance <= this.viewDistance;
}
+- }
++ }*/ // Paper - rewrite chunk system
+ }
+diff --git a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
+index 3dc1daa3c6a04d3ff1a2353773b465fc380994a2..4fa938e2d893c0db7d3fbd4c20b829cb895fa2f6 100644
+--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
+@@ -27,249 +27,113 @@ public abstract class GenerationChunkHolder {
+ public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk");
+ public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK);
+ protected final ChunkPos pos;
+- @Nullable
+- private volatile ChunkStatus highestAllowedStatus;
+- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>();
+- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size());
+- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>();
+- private final AtomicInteger generationRefCount = new AtomicInteger();
++ // Paper - rewrite chunk system
+
+ public GenerationChunkHolder(ChunkPos pos) {
+ this.pos = pos;
+ }
+
+ public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) {
+- if (this.isStatusDisallowed(requestedStatus)) {
+- return UNLOADED_CHUNK_FUTURE;
+- } else {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus);
+- if (completableFuture.isDone()) {
+- return completableFuture;
+- } else {
+- ChunkGenerationTask chunkGenerationTask = this.task.get();
+- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) {
+- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus);
+- }
+-
+- return completableFuture;
+- }
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) {
+- if (this.isStatusDisallowed(step.targetStatus())) {
+- return UNLOADED_CHUNK_FUTURE;
+- } else {
+- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> {
+- if (throwable != null) {
+- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading");
+- MinecraftServer.setFatalException(new ReportedException(crashReport));
+- } else {
+- this.completeFuture(step.targetStatus(), chunk);
+- }
+-
+- return ChunkResult.of(chunk);
+- }) : this.getOrCreateFuture(step.targetStatus());
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) {
+- ChunkStatus chunkStatus = this.highestAllowedStatus;
+- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel());
+- this.highestAllowedStatus = chunkStatus2;
+- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus));
+- if (bl) {
+- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus);
+- if (this.task.get() != null) {
+- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2));
+- }
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ public void replaceProtoChunk(ImposterProtoChunk chunk) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk));
+-
+- for (int i = 0; i < this.futures.length() - 1; i++) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i);
+- Objects.requireNonNull(completableFuture2);
+- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null);
+- if (!(chunkAccess instanceof ProtoChunk)) {
+- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess);
+- }
+-
+- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) {
+- throw new IllegalStateException("Future changed by other thread while trying to replace it");
+- }
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ void removeTask(ChunkGenerationTask loader) {
+- this.task.compareAndSet(loader, null);
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) {
+- ChunkGenerationTask chunkGenerationTask;
+- if (requestedStatus != null) {
+- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos());
+- } else {
+- chunkGenerationTask = null;
+- }
+-
+- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask);
+- if (chunkGenerationTask3 != null) {
+- chunkGenerationTask3.markForCancellation();
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) {
+- if (this.isStatusDisallowed(status)) {
+- return UNLOADED_CHUNK_FUTURE;
+- } else {
+- int i = status.getIndex();
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
+-
+- while (completableFuture == null) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>();
+- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2);
+- if (completableFuture == null) {
+- if (this.isStatusDisallowed(status)) {
+- this.failAndClearPendingFuture(i, completableFuture2);
+- return UNLOADED_CHUNK_FUTURE;
+- }
+-
+- return completableFuture2;
+- }
+- }
+-
+- return completableFuture;
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) {
+- int i = from == null ? 0 : from.getIndex() + 1;
+- int j = to.getIndex();
+-
+- for (int k = i; k <= j; k++) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k);
+- if (completableFuture != null) {
+- this.failAndClearPendingFuture(k, completableFuture);
+- }
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) {
+- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) {
+- throw new IllegalStateException("Nothing else should replace the future here");
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private void completeFuture(ChunkStatus status, ChunkAccess chunk) {
+- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk);
+- int i = status.getIndex();
+-
+- while (true) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
+- if (completableFuture == null) {
+- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) {
+- return;
+- }
+- } else {
+- if (completableFuture.complete(chunkResult)) {
+- return;
+- }
+-
+- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) {
+- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already");
+- }
+-
+- Thread.yield();
+- }
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ @Nullable
+ private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) {
+- if (checkUpperBound == null) {
+- return null;
+- } else {
+- ChunkStatus chunkStatus = checkUpperBound;
+-
+- for (ChunkStatus chunkStatus2 = this.startedWork.get();
+- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2);
+- chunkStatus = chunkStatus.getParent()
+- ) {
+- if (this.futures.get(chunkStatus.getIndex()) != null) {
+- return chunkStatus;
+- }
+-
+- if (chunkStatus == ChunkStatus.EMPTY) {
+- break;
+- }
+- }
+-
+- return null;
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private boolean acquireStatusBump(ChunkStatus nextStatus) {
+- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent();
+- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus);
+- if (chunkStatus2 == chunkStatus) {
+- return true;
+- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) {
+- return false;
+- } else {
+- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus);
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ private boolean isStatusDisallowed(ChunkStatus status) {
+- ChunkStatus chunkStatus = this.highestAllowedStatus;
+- return chunkStatus == null || status.isAfter(chunkStatus);
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ public void increaseGenerationRefCount() {
+- this.generationRefCount.incrementAndGet();
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ public void decreaseGenerationRefCount() {
+- int i = this.generationRefCount.decrementAndGet();
+- if (i < 0) {
+- throw new IllegalStateException("More releases than claims. Count: " + i);
+- }
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ public int getGenerationRefCount() {
+- return this.generationRefCount.get();
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ @Nullable
+ public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex());
+- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
++ return lastCompletion == null || !lastCompletion.genStatus().isOrAfter(requestedStatus) ? null : lastCompletion.chunk();
++ // Paper end - rewrite chunk system
+ }
+
+ @Nullable
+ public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
+- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
++ // Paper start - rewrite chunk system
++ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel());
++
++ if (requestedStatus.isOrAfter(maxStatus)) {
++ return null;
++ }
++
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
++ return lastCompletion == null || !lastCompletion.genStatus().isOrAfter(requestedStatus) ? null : lastCompletion.chunk();
++ // Paper end - rewrite chunk system
+ }
+
+ @Nullable
+ public ChunkAccess getLatestChunk() {
+- ChunkStatus chunkStatus = this.startedWork.get();
+- if (chunkStatus == null) {
+- return null;
+- } else {
+- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
+- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
+- }
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
++ return lastCompletion == null ? null : lastCompletion.chunk();
++ // Paper end - rewrite chunk system
+ }
+
+ @Nullable
+ public ChunkStatus getPersistedStatus() {
+- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex());
+- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
+- return chunkAccess == null ? null : chunkAccess.getPersistedStatus();
++ // Paper start - rewrite chunk system
++ final ChunkAccess chunk = this.getLatestChunk();
++ return chunk == null ? null : chunk.getPersistedStatus();
++ // Paper end - rewrite chunk system
+ }
+
+ public ChunkPos getPos() {
+@@ -277,7 +141,7 @@ public abstract class GenerationChunkHolder {
+ }
+
+ public FullChunkStatus getFullStatus() {
+- return ChunkLevel.fullStatus(this.getTicketLevel());
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system
+ }
+
+ public abstract int getTicketLevel();
+@@ -286,26 +150,15 @@ public abstract class GenerationChunkHolder {
+
+ @VisibleForDebug
+ public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
+- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>();
+-
+- for (int i = 0; i < CHUNK_STATUSES.size(); i++) {
+- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i)));
+- }
+-
+- return list;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ @Nullable
+ @VisibleForDebug
+ public ChunkStatus getLatestStatus() {
+- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) {
+- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i);
+- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
+- if (chunkAccess != null) {
+- return chunkStatus;
+- }
+- }
+-
+- return null;
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
++ return lastCompletion == null ? null : lastCompletion.genStatus();
++ // Paper end - rewrite chunk system
}
-+ */ // Paper - rewrite chunk system
}
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e22e8b4d6 100644
+index 6f506ed8c8052f56356f60c5987cca8aa34d1d78..ade744dd17431cc671de1322d7f58b54039fe1c9 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -71,7 +71,7 @@ public class ServerChunkCache extends ChunkSource {
- public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
- final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock();
- final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f);
-- long chunkFutureAwaitCounter;
-+ final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - chunk system rewrite
+@@ -75,6 +75,29 @@ public class ServerChunkCache extends ChunkSource {
+ long chunkFutureAwaitCounter;
private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4];
// Paper end
++ // Paper start - rewrite chunk system
++ private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
++ final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>();
++ chunkTaskScheduler.scheduleChunkLoad(
++ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING,
++ completable::complete
++ );
++
++ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ);
++ this.mainThreadProcessor.managedBlock(completable::isDone);
++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait();
++ }
++
++ final ChunkAccess ret = completable.join();
++ if (ret == null) {
++ throw new IllegalStateException("Chunk not loaded when requested");
++ }
++
++ return ret;
++ }
++ // Paper end - rewrite chunk system
-@@ -195,7 +195,7 @@ public class ServerChunkCache extends ChunkSource {
- public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
- long k = ChunkPos.asLong(x, z);
-
-- if (Thread.currentThread() == this.mainThread) {
-+ if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system
- return this.getChunkAtIfLoadedMainThread(x, z);
- }
-
-@@ -247,7 +247,8 @@ public class ServerChunkCache extends ChunkSource {
+ public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
+ this.level = world;
+@@ -248,63 +271,36 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@Override
public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
- if (Thread.currentThread() != this.mainThread) {
-+ final int x1 = x; final int z1 = z; // Paper - conflict on variable change
-+ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system
- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
- return this.getChunk(x, z, leastStatus, create);
- }, this.mainThreadProcessor).join();
-@@ -263,15 +264,7 @@ public class ServerChunkCache extends ChunkSource {
- gameprofilerfiller.incrementCounter("getChunk");
- long k = ChunkPos.asLong(x, z);
+- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
+- return this.getChunk(x, z, leastStatus, create);
+- }, this.mainThreadProcessor).join();
+- } else {
+- // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
+- LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
+- if (ifLoaded != null) {
+- return ifLoaded;
+- }
+- // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
+- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+- gameprofilerfiller.incrementCounter("getChunk");
+- long k = ChunkPos.asLong(x, z);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
- for (int l = 0; l < 4; ++l) {
- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
@@ -18099,59 +24757,68 @@ index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e
- return ichunkaccess;
- }
- }
-- }
-+ // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system
-
- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
-@@ -279,9 +272,11 @@ public class ServerChunkCache extends ChunkSource {
-
- Objects.requireNonNull(completablefuture);
- if (!completablefuture.isDone()) { // Paper
-+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system
- com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
- this.level.timings.syncChunkLoad.startTiming(); // Paper
- chunkproviderserver_b.managedBlock(completablefuture::isDone);
-+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - rewrite chunk system
- this.level.timings.syncChunkLoad.stopTiming(); // Paper
- } // Paper
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
-@@ -299,7 +294,7 @@ public class ServerChunkCache extends ChunkSource {
++ if (leastStatus == ChunkStatus.FULL) {
++ if (currentChunk != null && currentChunk.isFullChunkReady() && (currentChunk.getCurrentChunk() instanceof LevelChunk fullChunk)) {
++ return fullChunk;
++ } else if (!create) {
++ return null;
+ }
+-
+- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
+- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
+- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
+-
+- Objects.requireNonNull(completablefuture);
+- if (!completablefuture.isDone()) { // Paper
+- com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
+- this.level.timings.syncChunkLoad.startTiming(); // Paper
+- chunkproviderserver_b.managedBlock(completablefuture::isDone);
+- this.level.timings.syncChunkLoad.stopTiming(); // Paper
+- } // Paper
+- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
+- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
+-
+- if (ichunkaccess1 == null && create) {
+- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
+- } else {
+- this.storeInCache(k, ichunkaccess1, leastStatus);
+- return ichunkaccess1;
++ return this.syncLoad(x, z, leastStatus);
++ } else {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion;
++ if (currentChunk != null && (lastCompletion = currentChunk.getLastChunkCompletion()) != null &&
++ lastCompletion.genStatus().isOrAfter(leastStatus)) {
++ return lastCompletion.chunk();
++ } else if (!create) {
++ return null;
+ }
++ return this.syncLoad(x, z, leastStatus);
+ }
++ // Paper end - rewrite chunk system
+ }
+
@Nullable
@Override
public LevelChunk getChunkNow(int chunkX, int chunkZ) {
- if (Thread.currentThread() != this.mainThread) {
-+ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system
- return null;
- } else {
- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
-@@ -313,7 +308,7 @@ public class ServerChunkCache extends ChunkSource {
+- return null;
+- } else {
+- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
+- }
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); // Paper - rewrite chunk system
}
- public CompletableFuture<ChunkResult<ChunkAccess>> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
-- boolean flag1 = Thread.currentThread() == this.mainThread;
-+ boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
- CompletableFuture completablefuture;
-
- if (flag1) {
-@@ -333,48 +328,54 @@ public class ServerChunkCache extends ChunkSource {
- return completablefuture;
+ private void clearCache() {
+@@ -335,35 +331,42 @@ public class ServerChunkCache extends ChunkSource {
}
-+
private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
- long k = chunkcoordintpair.toLong();
- int l = ChunkLevel.byStatus(leastStatus);
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
-+ // Paper start - add isUrgent - old sig left in place for dirty nms plugins
-+ return getChunkFutureMainThread(chunkX, chunkZ, leastStatus, create, false);
-+ }
-+ private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create, boolean isUrgent) {
+ // Paper start - rewrite chunk system
+ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
-+ int minLevel = ChunkLevel.byStatus(leastStatus);
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
- // CraftBukkit start - don't add new ticket for currently unloading chunk
- boolean currentlyUnloading = false;
@@ -18159,7 +24826,10 @@ index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e
- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
-+ boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
++ final int minLevel = ChunkLevel.byStatus(leastStatus);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
++
++ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
+
+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) {
+ return ChunkHolder.UNLOADED_CHUNK_FUTURE;
@@ -18177,13 +24847,13 @@ index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e
- if (this.chunkAbsent(playerchunk, l)) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
+
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion();
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion();
+ if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(leastStatus)) {
+ // schedule
+ CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
+ Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> {
+ if (chunk == null) {
-+ ret.complete(ChunkResult.error("Unexpected chunk unload"));
++ ret.complete(ChunkHolder.UNLOADED_CHUNK);
+ } else {
+ ret.complete(ChunkResult.of(chunk));
}
@@ -18191,16 +24861,13 @@ index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e
- }
+ };
-- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap);
-- }
-+ this.level.chunkTaskScheduler.scheduleChunkLoad(
+- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap);
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ chunkX, chunkZ, leastStatus, true,
-+ isUrgent ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
+ complete
+ );
-
-- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
-- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
++
+ return ret;
+ } else {
+ // can return now
@@ -18209,556 +24876,420 @@ index 0e89cf0742b9443f5256081987242554de24d893..802e9d266c01eaf8a83e78fe8dbe881e
+ // Paper end - rewrite chunk system
}
-+ // Paper - rewrite chunk system
-+
+ private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
+@@ -372,19 +375,23 @@ public class ServerChunkCache extends ChunkSource {
+
@Override
public boolean hasChunk(int x, int z) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong());
- int k = ChunkLevel.byStatus(ChunkStatus.FULL);
-
- return !this.chunkAbsent(playerchunk, k);
-+ return this.getChunkAtIfLoadedImmediately(x, z) != null; // Paper - rewrite chunk system
++ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system
}
@Nullable
-@@ -386,22 +387,13 @@ public class ServerChunkCache extends ChunkSource {
- if (playerchunk == null) {
- return null;
- } else {
-- int l = ServerChunkCache.CHUNK_STATUSES.size() - 1;
--
-- while (true) {
-- ChunkStatus chunkstatus = (ChunkStatus) ServerChunkCache.CHUNK_STATUSES.get(l);
-- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) playerchunk.getFutureIfPresentUnchecked(chunkstatus).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null);
--
-- if (ichunkaccess != null) {
-- return ichunkaccess;
-- }
--
-- if (chunkstatus == ChunkStatus.INITIALIZE_LIGHT.getParent()) {
-- return null;
-- }
+ @Override
+ public LightChunk getChunkForLighting(int chunkX, int chunkZ) {
+- long k = ChunkPos.asLong(chunkX, chunkZ);
+- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
-
-- --l;
-+ // Paper start - rewrite chunk system
-+ ChunkStatus status = playerchunk.getChunkHolderStatus();
-+ if (status != null && !status.isOrAfter(ChunkStatus.LIGHT.getParent())) {
-+ return null;
- }
-+ return playerchunk.getAvailableChunkNow();
-+ // Paper end - rewrite chunk system
- }
+- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
++ if (newChunkHolder == null) {
++ return null;
++ }
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
++ if (lastCompletion == null || !lastCompletion.genStatus().isOrAfter(ChunkStatus.INITIALIZE_LIGHT)) {
++ return null;
++ }
++ return lastCompletion.chunk();
++ // Paper end - rewrite chunk system
}
-@@ -415,15 +407,7 @@ public class ServerChunkCache extends ChunkSource {
+ @Override
+@@ -397,16 +404,7 @@ public class ServerChunkCache extends ChunkSource {
}
public boolean runDistanceManagerUpdates() { // Paper - public
- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
- boolean flag1 = this.chunkMap.promoteChunkMap();
-
+- this.chunkMap.runGenerationTasks();
- if (!flag && !flag1) {
- return false;
- } else {
- this.clearCache();
- return true;
- }
-+ return this.level.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
}
// Paper start
-@@ -433,9 +417,10 @@ public class ServerChunkCache extends ChunkSource {
+@@ -416,13 +414,14 @@ public class ServerChunkCache extends ChunkSource {
// Paper end
public boolean isPositionTicking(long pos) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess());
-+ // Paper start - replace player chunk loader system
-+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos);
-+ return holder != null && holder.isTickingReady();
-+ // Paper end - replace player chunk loader system
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
++ return newChunkHolder != null && newChunkHolder.isTickingReady();
++ // Paper end - rewrite chunk system
}
public void save(boolean flush) {
-@@ -451,17 +436,13 @@ public class ServerChunkCache extends ChunkSource {
- this.close(true);
+- this.runDistanceManagerUpdates();
++ // Paper - rewrite chunk system
+ try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings
+ this.chunkMap.saveAllChunks(flush);
+ } // Paper - Timings
+@@ -435,12 +434,7 @@ public class ServerChunkCache extends ChunkSource {
}
-- public void close(boolean save) throws IOException {
+ public void close(boolean save) throws IOException {
- if (save) {
- this.save(true);
- }
- // CraftBukkit end
- this.lightEngine.close();
- this.chunkMap.close();
-+ public void close(boolean save) { // Paper - rewrite chunk system
-+ this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(save, true); // Paper - rewrite chunk system
}
// CraftBukkit start - modelled on below
- public void purgeUnload() {
-+ if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system
- this.level.getProfiler().push("purge");
- this.distanceManager.purgeStaleTickets();
- this.runDistanceManagerUpdates();
-@@ -485,6 +466,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -468,6 +462,7 @@ public class ServerChunkCache extends ChunkSource {
this.level.getProfiler().popPush("chunks");
if (tickChunks) {
this.level.timings.chunks.startTiming(); // Paper - timings
-+ this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system
this.tickChunks();
this.level.timings.chunks.stopTiming(); // Paper - timings
this.chunkMap.tick();
-@@ -587,7 +569,12 @@ public class ServerChunkCache extends ChunkSource {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
+@@ -567,11 +562,12 @@ public class ServerChunkCache extends ChunkSource {
+ }
- if (playerchunk != null) {
+ private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
+- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
+-
+- if (playerchunk != null) {
- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer);
-+ // Paper start - rewrite chunk system
-+ LevelChunk chunk = playerchunk.getFullChunkNow();
-+ if (chunk != null) {
-+ chunkConsumer.accept(chunk);
-+ }
-+ // Paper end - rewrite chunk system
++ // Paper start - rewrite chunk system
++ final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos));
++ if (fullChunk != null) {
++ chunkConsumer.accept(fullChunk);
}
++ // Paper end - rewrite chunk system
+
+ }
+
+@@ -665,6 +661,12 @@ public class ServerChunkCache extends ChunkSource {
+ this.chunkMap.setServerViewDistance(watchDistance);
+ }
++ // Paper start - rewrite chunk system
++ public void setSendViewDistance(int viewDistance) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setSendDistance(viewDistance);
++ }
++ // Paper end - rewrite chunk system
++
+ public void setSimulationDistance(int simulationDistance) {
+ this.distanceManager.updateSimulationDistance(simulationDistance);
}
-@@ -753,17 +740,10 @@ public class ServerChunkCache extends ChunkSource {
+@@ -743,16 +745,14 @@ public class ServerChunkCache extends ChunkSource {
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
- try {
- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
+- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
++ // Paper start - rewrite chunk system
++ final ServerChunkCache serverChunkCache = ServerChunkCache.this;
++ if (serverChunkCache.runDistanceManagerUpdates()) {
return true;
-- } else {
+ } else {
- ServerChunkCache.this.lightEngine.tryScheduleUpdate();
- return super.pollTask();
++ return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask();
}
- } finally {
- ServerChunkCache.this.chunkMap.callbackExecutor.run();
- }
-- // CraftBukkit end
-+ return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system
++ // Paper end - rewrite chunk system
+ // CraftBukkit end
}
}
-
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4a45b37fa 100644
+index 4d7e234d379a451c4bb53bc2fcdf22cb191f8d1a..37971d9fc59ecf3736fccf7a27f17e37a56efeb9 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -199,7 +199,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -184,7 +184,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
+ import org.bukkit.event.world.TimeSkipEvent;
+ // CraftBukkit end
+
+-public class ServerLevel extends Level implements WorldGenLevel {
++public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader { // Paper - rewrite chunk system
+
+ public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
+ public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
+@@ -200,7 +200,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
public final PrimaryLevelData serverLevelData; // CraftBukkit - type
private int lastSpawnChunkRadius;
final EntityTickList entityTickList;
- public final PersistentEntitySectionManager<Entity> entityManager;
-+ //public final PersistentEntitySectionManager<Entity> entityManager; // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
private final GameEventDispatcher gameEventDispatcher;
public boolean noSave;
private final SleepStatus sleepStatus;
-@@ -268,50 +268,65 @@ public class ServerLevel extends Level implements WorldGenLevel {
- return true;
+@@ -339,6 +339,163 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ return player != null && player.level() == this ? player : null;
}
-
-- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-- java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-- if (Thread.currentThread() != this.thread) {
-- this.getChunkSource().mainThreadProcessor.execute(() -> {
-- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
-- });
-- return;
-- }
-+ public final void loadChunksAsync(BlockPos pos, int radiusBlocks,
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ loadChunksAsync(
-+ (pos.getX() - radiusBlocks) >> 4,
-+ (pos.getX() + radiusBlocks) >> 4,
-+ (pos.getZ() - radiusBlocks) >> 4,
-+ (pos.getZ() + radiusBlocks) >> 4,
-+ priority, onLoad
-+ );
-+ }
-+
-+ public final void loadChunksAsync(BlockPos pos, int radiusBlocks,
-+ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus,
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ loadChunksAsync(
-+ (pos.getX() - radiusBlocks) >> 4,
-+ (pos.getX() + radiusBlocks) >> 4,
-+ (pos.getZ() - radiusBlocks) >> 4,
-+ (pos.getZ() + radiusBlocks) >> 4,
-+ chunkStatus, priority, onLoad
-+ );
-+ }
-+
-+ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ,
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
-+ }
-+
-+ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ,
-+ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus,
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
- List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
-- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
--
-- int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
-- int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
--
-- int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
-- int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
--
-- int minChunkX = minBlockX >> 4;
-- int maxChunkX = maxBlockX >> 4;
--
-- int minChunkZ = minBlockZ >> 4;
-- int maxChunkZ = maxBlockZ >> 4;
-
- ServerChunkCache chunkProvider = this.getChunkSource();
-
- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
-- int[] loadedChunks = new int[1];
-+ java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
-
-- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
-+ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter.getAndIncrement());
-+
-+ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(chunkStatus);
-
- java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
- if (chunk != null) {
-- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
-+ synchronized (ret) {
- ret.add(chunk);
-- ticketLevels.add(ticketLevel);
-+ }
- chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
- }
-- if (++loadedChunks[0] == requiredChunks) {
-+ if (loadedChunks.incrementAndGet() == requiredChunks) {
- try {
- onLoad.accept(java.util.Collections.unmodifiableList(ret));
- } finally {
- for (int i = 0, len = ret.size(); i < len; ++i) {
- ChunkPos chunkPos = ret.get(i).getPos();
-- int ticketLevel = ticketLevels.getInt(i);
-
- chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
- chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
-@@ -323,12 +338,228 @@ public class ServerLevel extends Level implements WorldGenLevel {
- for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
- for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
- io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(
-- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
-+ this, cx, cz, chunkStatus, true, priority, consumer
- );
- }
- }
- }
-- // Paper end
-+
-+ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
-+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+
-+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
-+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
-+
-+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
-+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
-+
-+ int minChunkX = minBlockX >> 4;
-+ int maxChunkX = maxBlockX >> 4;
-+
-+ int minChunkZ = minBlockZ >> 4;
-+ int maxChunkZ = maxBlockZ >> 4;
-+
-+ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad);
-+ }
-+
+ // Paper end - optimise getPlayerByUUID
+ // Paper start - rewrite chunk system
-+ public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
-+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController chunkDataControllerNew
-+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA) {
-+
-+ @Override
-+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() {
-+ return ServerLevel.this.getChunkSource().chunkMap.regionFileCache;
-+ }
-+
-+ @Override
-+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException {
-+ ServerLevel.this.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound);
-+ }
-+
-+ @Override
-+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException {
-+ return ServerLevel.this.getChunkSource().chunkMap.readSync(new ChunkPos(chunkX, chunkZ));
-+ }
-+ };
-+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController poiDataControllerNew
-+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA) {
++ private boolean markedClosing;
++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this);
++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController;
++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController;
++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController;
++ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
+
-+ @Override
-+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() {
-+ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager();
-+ }
-+
-+ @Override
-+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException {
-+ ServerLevel.this.getChunkSource().chunkMap.getPoiManager().write(new ChunkPos(chunkX, chunkZ), compound);
-+ }
-+
-+ @Override
-+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException {
-+ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager().read(new ChunkPos(chunkX, chunkZ));
-+ }
-+ };
-+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController entityDataControllerNew
-+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA) {
-+
-+ @Override
-+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() {
-+ return ServerLevel.this.entityStorage;
-+ }
-+
-+ @Override
-+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException {
-+ ServerLevel.this.writeEntityChunk(chunkX, chunkZ, compound);
++ @Override
++ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (!newChunkHolder.isFullChunkReady()) {
++ return null;
+ }
+
-+ @Override
-+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException {
-+ return ServerLevel.this.readEntityChunk(chunkX, chunkZ);
++ if (newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
++ return levelChunk;
+ }
-+ };
-+ private final EntityRegionFileStorage entityStorage;
-+
-+ private static final class EntityRegionFileStorage extends net.minecraft.world.level.chunk.storage.RegionFileStorage {
++ // race condition: chunk unloaded, only happens off-main
++ return null;
++ }
+
-+ public EntityRegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
-+ super(storageKey, directory, dsync);
++ @Override
++ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ if (newChunkHolder == null) {
++ return null;
+ }
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
++ return lastCompletion == null ? null : lastCompletion.chunk();
++ }
+
-+ protected void write(ChunkPos pos, net.minecraft.nbt.CompoundTag nbt) throws IOException {
-+ ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
-+ if (nbtPos != null && !pos.equals(nbtPos)) {
-+ throw new IllegalArgumentException(
-+ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
-+ + " but compound says coordinate is " + nbtPos + " for world: " + this
-+ );
-+ }
-+ super.write(pos, nbt);
++ @Override
++ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
++ if (newChunkHolder == null) {
++ return null;
+ }
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
++ return lastCompletion == null || !lastCompletion.genStatus().isOrAfter(leastStatus) ? null : lastCompletion.chunk();
+ }
+
-+ private void writeEntityChunk(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException {
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave(
-+ this, chunkX, chunkZ, compound,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA);
-+ return;
-+ }
-+ this.entityStorage.write(new ChunkPos(chunkX, chunkZ), compound);
++ @Override
++ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
++ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
+ }
+
-+ private net.minecraft.nbt.CompoundTag readEntityChunk(int chunkX, int chunkZ) throws IOException {
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData(
-+ this, chunkX, chunkZ, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
-+ );
-+ }
-+ return this.entityStorage.read(new ChunkPos(chunkX, chunkZ));
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
++ return this.chunkTaskScheduler;
+ }
+
-+ private final io.papermc.paper.chunk.system.entity.EntityLookup entityLookup;
-+ public final io.papermc.paper.chunk.system.entity.EntityLookup getEntityLookup() {
-+ return this.entityLookup;
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() {
++ return this.chunkDataController;
+ }
+
-+ private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong();
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() {
++ return this.poiDataController;
++ }
+
-+ private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) {
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded =
-+ this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion;
-+ if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) {
-+ return loadedCompletion.chunk();
-+ }
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() {
++ return this.entityDataController;
++ }
+
-+ return null;
++ @Override
++ public final int moonrise$getRegionChunkShift() {
++ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
+ }
+
+ @Override
-+ public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) {
-+ if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL)) {
-+ throw new IllegalArgumentException("Status: " + status);
-+ }
-+ ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status);
-+ if (loaded != null) {
-+ return loaded;
-+ }
++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() {
++ return this.chunkLoader;
++ }
+
-+ Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement());
-+ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status);
-+ this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
-+ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId
++ @Override
++ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
++ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ this.moonrise$loadChunksAsync(
++ (pos.getX() - radiusBlocks) >> 4,
++ (pos.getX() + radiusBlocks) >> 4,
++ (pos.getZ() - radiusBlocks) >> 4,
++ (pos.getZ() + radiusBlocks) >> 4,
++ priority, onLoad
+ );
-+ this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
-+
-+ this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING);
-+
-+ // we could do a simple spinwait here, since we do not need to process tasks while performing this load
-+ // but we process tasks only because it's a better use of the time spent
-+ this.chunkSource.mainThreadProcessor.managedBlock(() -> {
-+ return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null;
-+ });
-+
-+ loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status);
-+ if (loaded == null) {
-+ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
-+ }
++ }
+
-+ this.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
-+ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId
++ @Override
++ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
++ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ this.moonrise$loadChunksAsync(
++ (pos.getX() - radiusBlocks) >> 4,
++ (pos.getX() + radiusBlocks) >> 4,
++ (pos.getZ() - radiusBlocks) >> 4,
++ (pos.getZ() + radiusBlocks) >> 4,
++ chunkStatus, priority, onLoad
+ );
-+
-+ return loaded;
+ }
+
-+ public final int getRegionChunkShift() {
-+ // placeholder for folia
-+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
++ @Override
++ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
++ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
+ }
-+ // Paper end - rewrite chunk system
+
-+ public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this);
-+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
++ @Override
++ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
++ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
-+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
-+ return this.viewDistances.get();
-+ }
++ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
++ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
++ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
++ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
+
-+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
-+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
-+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
-+ return;
++ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
++
++ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
++ if (chunk != null) {
++ synchronized (ret) {
++ ret.add(chunk);
++ }
++ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier);
+ }
-+ }
-+ }
++ if (loadedChunks.incrementAndGet() == requiredChunks) {
++ try {
++ onLoad.accept(java.util.Collections.unmodifiableList(ret));
++ } finally {
++ for (int i = 0, len = ret.size(); i < len; ++i) {
++ final ChunkPos chunkPos = ret.get(i).getPos();
+
-+ public void setTickViewDistance(final int distance) {
-+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
-+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
-+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setTickViewDistance(distance);
-+ });
-+ }
++ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier);
++ }
++ }
++ }
++ };
+
-+ public void setLoadViewDistance(final int distance) {
-+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
++ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer);
++ }
+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setLoadViewDistance(distance);
-+ });
+ }
+
-+ public void setSendViewDistance(final int distance) {
-+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
-+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setSendViewDistance(distance);
-+ });
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
++ return this.viewDistanceHolder;
+ }
++ // Paper end - rewrite chunk system
- // Paper start - optimise getPlayerByUUID
- @Nullable
-@@ -382,16 +613,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
- // CraftBukkit end
- boolean flag2 = minecraftserver.forceSynchronousWrites();
+ // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
+ public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
+@@ -385,14 +542,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
DataFixer datafixer = minecraftserver.getFixerUpper();
-- EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
-+ this.entityStorage = new EntityRegionFileStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system
+ EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
-+ // this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
int j = this.spigotConfig.viewDistance; // Spigot
int k = this.spigotConfig.simulationDistance; // Spigot
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-+ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
- Objects.requireNonNull(this.entityManager);
- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
-+ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system
+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
return minecraftserver.overworld().getDataStorage();
});
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
-@@ -420,6 +651,9 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -420,6 +576,19 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
});
++ // Paper start - rewrite chunk system
++ this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
++ new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
++ new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"),
++ convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"),
++ minecraftserver.forceSynchronousWrites()
++ )
++ );
++ this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this);
++ this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this);
++ this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
++ this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this, ca.spottedleaf.moonrise.common.util.MoonriseCommon.WORKER_POOL);
++ // Paper end - rewrite chunk system
this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
-+
-+ this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system
-+ this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system
}
- // Paper start
-@@ -552,7 +786,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -553,7 +722,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
gameprofilerfiller.push("checkDespawn");
entity.checkDespawn();
gameprofilerfiller.pop();
- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
-+ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list
++ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - rewrite chunk system
Entity entity1 = entity.getVehicle();
if (entity1 != null) {
-@@ -577,13 +811,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -578,13 +747,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
gameprofilerfiller.push("entityManagement");
- this.entityManager.tick();
-+ //this.entityManager.tick(); // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
gameprofilerfiller.pop();
}
@Override
public boolean shouldTickBlocksAt(long chunkPos) {
- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
-+ // Paper start - replace player chunk loader system
-+ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos);
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ return holder != null && holder.isTickingReady();
-+ // Paper end - replace player chunk loader system
++ // Paper end - rewrite chunk system
}
protected void tickTime() {
-@@ -1060,6 +1297,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1061,6 +1233,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
-+ // Paper start - rewrite chunk system - add close param
++ // Paper start - add close param
+ this.save(progressListener, flush, savingDisabled, false);
+ }
+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) {
-+ // Paper end - rewrite chunk system - add close param
++ // Paper end - add close param
ServerChunkCache chunkproviderserver = this.getChunkSource();
if (!savingDisabled) {
-@@ -1075,16 +1317,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1076,16 +1253,21 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
timings.worldSaveChunks.startTiming(); // Paper
- chunkproviderserver.save(flush);
-+ if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system
-+ if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system
++ if (!close) { chunkproviderserver.save(flush); } // Paper - add close param
timings.worldSaveChunks.stopTiming(); // Paper
}// Paper
- if (flush) {
@@ -18766,55 +25297,62 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
- } else {
- this.entityManager.autoSave();
- }
-+ // Paper - rewrite chunk system - entity saving moved into ChunkHolder
++ // Paper - rewrite chunk system
-- }
-+ } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system
+ }
++ // Paper start - add close param
++ if (close) {
++ try {
++ chunkproviderserver.close(!savingDisabled);
++ } catch (IOException never) {
++ throw new RuntimeException(never);
++ }
++ }
++ // Paper end - add close param
// CraftBukkit start - moved from MinecraftServer.saveChunks
ServerLevel worldserver1 = this;
-@@ -1220,7 +1459,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1218,7 +1400,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
}
- this.entityManager.addNewEntity(player);
-+ this.entityLookup.addNewEntity(player); // Paper - rewite chunk system
++ this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system
}
// CraftBukkit start
-@@ -1251,7 +1490,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1249,7 +1431,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
// CraftBukkit end
- return this.entityManager.addNewEntity(entity);
-+ return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system
++ return this.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system
}
}
-@@ -1263,10 +1502,10 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1260,11 +1442,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+
public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
// CraftBukkit end
- Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
+- Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-+ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system
-
+-
- Objects.requireNonNull(this.entityManager);
- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
-+ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system
-+ if (stream.anyMatch(this.entityLookup::hasEntity)) { // Paper - rewrite chunk system
++ if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
return false;
} else {
this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
-@@ -1852,7 +2091,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1850,7 +2028,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
}
- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
-+ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system
++ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
-@@ -1901,7 +2140,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1899,7 +2077,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
try {
@@ -18823,7 +25361,7 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
} catch (Throwable throwable4) {
if (bufferedwriter2 != null) {
try {
-@@ -1922,7 +2161,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -1920,7 +2098,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
try {
@@ -18832,42 +25370,50 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
} catch (Throwable throwable6) {
if (bufferedwriter3 != null) {
try {
-@@ -2064,7 +2303,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -2062,7 +2240,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
@VisibleForTesting
public String getWatchdogStats() {
- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> {
-+ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system
++ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system
return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
}), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats());
}
-@@ -2124,15 +2363,15 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -2092,15 +2270,25 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public LevelEntityGetter<Entity> getEntities() {
org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- return this.entityManager.getEntityGetter();
-+ return this.entityLookup; // Paper - rewrite chunk system
++ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
}
-- public void addLegacyChunkEntities(Stream<Entity> entities) {
+ public void addLegacyChunkEntities(Stream<Entity> entities) {
- this.entityManager.addLegacyChunkEntities(entities);
-+ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos forChunk) { // Paper - rewrite chunk system
-+ this.entityLookup.addLegacyChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system
++ // Paper start - add chunkpos param
++ this.addLegacyChunkEntities(entities, null);
++ }
++ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
++ // Paper end - add chunkpos param
++ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
-- public void addWorldGenChunkEntities(Stream<Entity> entities) {
+ public void addWorldGenChunkEntities(Stream<Entity> entities) {
- this.entityManager.addWorldGenChunkEntities(entities);
-+ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos forChunk) { // Paper - rewrite chunk system
-+ this.entityLookup.addWorldGenChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system
++ // Paper start - add chunkpos param
++ this.addWorldGenChunkEntities(entities, null);
++ }
++ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
++ // Paper end - add chunkpos param
++ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
public void startTickingChunk(LevelChunk chunk) {
-@@ -2152,34 +2391,49 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -2120,34 +2308,47 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public void close() throws IOException {
super.close();
- this.entityManager.close();
-+ //this.entityManager.close(); // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
}
@Override
@@ -18875,29 +25421,27 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
String s = this.chunkSource.gatherStats();
- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats();
-+ return "Chunks[S] W: " + s + " E: " + this.entityLookup.getDebugInfo(); // Paper - rewrite chunk system
++ return "Chunks[S] W: " + s + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
}
public boolean areEntitiesLoaded(long chunkPos) {
- return this.entityManager.areEntitiesLoaded(chunkPos);
-+ // Paper start - rewrite chunk system
-+ return this.getChunkIfLoadedImmediately(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos)) != null;
-+ // Paper end - rewrite chunk system
++ return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
}
private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos);
-+ // Paper start - optimize is ticking ready type functions
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos);
++ // Paper start - rewrite chunk system
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
+ return chunkHolder != null && chunkHolder.isTickingReady();
-+ // Paper end
++ // Paper end - rewrite chunk system
}
public boolean isPositionEntityTicking(BlockPos pos) {
- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos));
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos));
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
@@ -18905,7 +25449,7 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
public boolean isNaturalSpawningAllowed(BlockPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos));
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
@@ -18913,137 +25457,224 @@ index b33bf957b1541756e3b983b87b1c83629757739a..0ccdc8d135dd3edb410fbc1d248c20a4
public boolean isNaturalSpawningAllowed(ChunkPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos));
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
@Override
-@@ -2205,7 +2459,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -2173,7 +2374,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
crashreportsystemdetails.setDetail("Loaded entity count", () -> {
- return String.valueOf(this.entityManager.count());
-+ return String.valueOf(this.entityLookup.getAllCopy().length); // Paper
++ return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system
});
return crashreportsystemdetails;
}
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index b032ce115b98af0e0384fb88ca88075eb4ffac11..e2b72b07888e84fb4472920932b3feedbd4829b9 100644
+index 3cbb59df34156479d24a8251f2b3acbb5e60dc2c..6b9354e3ac064daa3101e71d8e54e883f628f70c 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -293,6 +293,50 @@ public class ServerPlayer extends Player {
+@@ -199,7 +199,7 @@ import org.bukkit.event.player.PlayerToggleSneakEvent;
+ import org.bukkit.inventory.MainHand;
+ // CraftBukkit end
+
+-public class ServerPlayer extends net.minecraft.world.entity.player.Player {
++public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
+@@ -297,6 +297,36 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
public @Nullable String clientBrandName = null; // Paper - Brand support
public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
-+ // Paper start - replace player chunk loader
-+ private final java.util.concurrent.atomic.AtomicReference<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1));
-+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
++ // Paper start - rewrite chunk system
++ private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+
-+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() {
-+ return this.viewDistances.get();
++ @Override
++ public final boolean moonrise$isRealPlayer() {
++ return this.isRealPlayer;
+ }
+
-+ private void updateViewDistance(final java.util.function.Function<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances, io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances> update) {
-+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) {
-+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) {
-+ return;
-+ }
-+ }
++ @Override
++ public final void moonrise$setRealPlayer(final boolean real) {
++ this.isRealPlayer = real;
+ }
+
-+ public void setTickViewDistance(final int distance) {
-+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) {
-+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance);
-+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setTickViewDistance(distance);
-+ });
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() {
++ return this.chunkLoader;
+ }
+
-+ public void setLoadViewDistance(final int distance) {
-+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
-+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setLoadViewDistance(distance);
-+ });
++ @Override
++ public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) {
++ this.chunkLoader = loader;
+ }
+
-+ public void setSendViewDistance(final int distance) {
-+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance);
-+ }
-+ this.updateViewDistance((input) -> {
-+ return input.setSendViewDistance(distance);
-+ });
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
++ return this.viewDistanceHolder;
+ }
-+ // Paper end - replace player chunk loader
++ // Paper end - rewrite chunk system
+
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
this.chatVisibility = ChatVisiblity.FULL;
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ffc7f596a4 100644
+index 63fae619e9b4ed49585f88ea7c167b0ee5efd859..4700c97487e176c670a3930564b621b4a6bf52bb 100644
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-@@ -37,15 +37,12 @@ import net.minecraft.world.level.chunk.status.ChunkStatus;
- public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
+@@ -23,15 +23,49 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
+ import net.minecraft.world.level.lighting.LevelLightEngine;
+ import org.slf4j.Logger;
+
+-public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
++public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system
public static final int DEFAULT_BATCH_SIZE = 1000;
private static final Logger LOGGER = LogUtils.getLogger();
- private final ProcessorMailbox<Runnable> taskMailbox;
- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
-+ // Paper - rewrite chunk system
++ // Paper - rewrite chunk sytem
private final ChunkMap chunkMap;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
-- private final int taskPerBatch = 1000;
++ // Paper - rewrite chunk sytem
+ private final int taskPerBatch = 1000;
- private final AtomicBoolean scheduled = new AtomicBoolean();
-+ // Paper - rewrite chunk system
++ // Paper - rewrite chunk sytem
++
++ // Paper start - rewrite chunk system
++ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong();
++ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
++ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) {
++ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
++
++ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ);
++ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
++ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
++ // chunk scheduling, we could be lighting and generating a chunk at the same time
++ return;
++ }
++
++ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get();
++
++ if (scheduledTask == null) {
++ // not scheduled
++ return;
++ }
++
++ if (!scheduledTask.markTicketAdded()) {
++ // ticket already added
++ return;
++ }
++
++ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement());
++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
++ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
++
++ scheduledTask.queueOrRunTask(() -> {
++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
++ });
++ }
++ // Paper end - rewrite chunk system
- // Paper start - replace light engine impl
-- protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine;
-+ public final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine;
- public final boolean hasBlockLight;
- public final boolean hasSkyLight;
- // Paper end - replace light engine impl
-@@ -59,8 +56,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+ public ThreadedLevelLightEngine(
+ LightChunkGetter chunkProvider,
+@@ -42,8 +76,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
) {
- super(chunkProvider, false, false); // Paper - destroy vanilla light engine state
- this.chunkMap = chunkStorage;
+ super(chunkProvider, true, hasBlockLight);
+ this.chunkMap = chunkLoadingManager;
- this.sorterMailbox = executor;
- this.taskMailbox = processor;
-+ // Paper - rewrite chunk system
- // Paper start - replace light engine impl
- this.hasBlockLight = true;
- this.hasSkyLight = hasBlockLight; // Nice variable name.
-@@ -104,7 +100,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- ++totalChunks;
- }
++ // Paper - rewrite chunk sytem
+ }
-- this.taskMailbox.tell(() -> {
-+ this.chunkMap.level.chunkTaskScheduler.radiusAwareScheduler.queueInfiniteRadiusTask(() -> { // Paper - rewrite chunk system
- this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
- chunkLightCallback.accept(chunkPos);
- ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
-@@ -121,7 +117,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
+ @Override
+@@ -57,164 +90,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
-- final Supplier<ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks> runnable) {
-+ final Supplier<io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks> runnable) { // Paper - rewrite chunk system
- final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
+ @Override
+ public void checkBlock(BlockPos pos) {
+- BlockPos blockPos = pos.immutable();
+- this.addTask(
+- SectionPos.blockToSectionCoord(pos.getX()),
+- SectionPos.blockToSectionCoord(pos.getZ()),
+- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
+- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
+- );
++ // Paper start - rewrite chunk system
++ final BlockPos posCopy = pos.immutable();
++ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
++ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy);
++ });
++ // Paper end - rewrite chunk system
+ }
- final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ);
-@@ -148,7 +144,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+ protected void updateChunkStatus(ChunkPos pos) {
+- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+- super.retainData(pos, false);
+- super.setLightEnabled(pos, false);
+-
+- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) {
+- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null);
+- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null);
+- }
+-
+- for (int j = this.levelHeightAccessor.getMinSection(); j < this.levelHeightAccessor.getMaxSection(); j++) {
+- super.updateSectionStatus(SectionPos.of(pos, j), true);
+- }
+- }, () -> "updateChunkStatus " + pos + " true"));
++ // Paper - rewrite chunk system
+ }
+
+ @Override
+ public void updateSectionStatus(SectionPos pos, boolean notReady) {
+- this.addTask(
+- pos.x(),
+- pos.z(),
+- () -> 0,
+- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
+- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
+- );
++ // Paper start - rewrite chunk system
++ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
++ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady);
++ });
++ // Paper end - rewrite chunk system
+ }
- final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ @Override
+ public void propagateLightSources(ChunkPos chunkPos) {
+- this.addTask(
+- chunkPos.x,
+- chunkPos.z,
+- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
+- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos)
+- );
++ // Paper - rewrite chunk system
+ }
-- final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get();
-+ final io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks updateFuture = runnable.get(); // Paper - rewrite chunk system
+ @Override
+ public void setLightEnabled(ChunkPos pos, boolean retainData) {
+- this.addTask(
+- pos.x,
+- pos.z,
+- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
+- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData)
+- );
++ // Paper start - rewrite chunk system
+ }
- if (updateFuture == null) {
- // not scheduled
-@@ -285,16 +281,11 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+ @Override
+ public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
+- this.addTask(
+- pos.x(),
+- pos.z(),
+- () -> 0,
+- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
+- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos)
+- );
++ // Paper start - rewrite chunk system
}
private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
@@ -19062,41 +25693,35 @@ index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ff
}
@Override
-@@ -327,83 +318,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
+ public void retainData(ChunkPos pos, boolean retainData) {
+- this.addTask(
+- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
+- );
++ // Paper start - rewrite chunk system
}
- public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
-- // Paper start - replace light engine impl
-- if (true) {
-- boolean lit = excludeBlocks;
-- final ChunkPos chunkPos = chunk.getPos();
--
-- return CompletableFuture.supplyAsync(() -> {
-- final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk);
-- if (!lit) {
-- chunk.setLightCorrect(false);
-- this.theLightEngine.lightChunk(chunk, emptySections);
-- chunk.setLightCorrect(true);
-- } else {
-- this.theLightEngine.forceLoadInChunk(chunk, emptySections);
-- // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have
-- // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should
-- // catch what we miss here.
-- this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z);
-- }
+ public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
+- ChunkPos chunkPos = chunk.getPos();
+- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+- LevelChunkSection[] levelChunkSections = chunk.getSections();
-
-- this.chunkMap.releaseLightTicket(chunkPos);
-- return chunk;
-- }, (runnable) -> {
-- this.theLightEngine.scheduleChunkLight(chunkPos, runnable);
-- this.tryScheduleUpdate();
-- }).whenComplete((final ChunkAccess c, final Throwable throwable) -> {
-- if (throwable != null) {
-- LOGGER.error("Failed to light chunk " + chunkPos, throwable);
+- for (int i = 0; i < chunk.getSectionsCount(); i++) {
+- LevelChunkSection levelChunkSection = levelChunkSections[i];
+- if (!levelChunkSection.hasOnlyAir()) {
+- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
+- super.updateSectionStatus(SectionPos.of(chunkPos, j), false);
- }
-- });
-- }
-- // Paper end - replace light engine impl
+- }
+- }, () -> "initializeLight: " + chunkPos));
+- return CompletableFuture.supplyAsync(() -> {
+- super.setLightEnabled(chunkPos, bl);
+- super.retainData(chunkPos, false);
+- return chunk;
+- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
++ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system
+ }
+
+ public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
- ChunkPos chunkPos = chunk.getPos();
- chunk.setLightCorrect(false);
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
@@ -19106,14 +25731,13 @@ index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ff
- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks));
- return CompletableFuture.supplyAsync(() -> {
- chunk.setLightCorrect(true);
-- this.chunkMap.releaseLightTicket(chunkPos);
- return chunk;
- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void tryScheduleUpdate() {
-- if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine
+- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
- this.taskMailbox.tell(() -> {
- this.runUpdate();
- this.scheduled.set(false);
@@ -19135,7 +25759,7 @@ index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ff
- }
-
- objectListIterator.back(j);
-- this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
+- super.runLightUpdates();
-
- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
@@ -19149,26 +25773,46 @@ index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ff
}
public CompletableFuture<?> waitForPendingTasks(int x, int z) {
+- return CompletableFuture.runAsync(() -> {
+- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback));
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ static enum TaskType {
diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
-index eba83b085435150e5954fd5d41dda9ce1d0601ad..e97329f867de2acbdd666925ba5d2aafa7a90574 100644
+index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644
--- a/src/main/java/net/minecraft/server/level/Ticket.java
+++ b/src/main/java/net/minecraft/server/level/Ticket.java
-@@ -6,9 +6,12 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+@@ -2,13 +2,25 @@ package net.minecraft.server.level;
+
+ import java.util.Objects;
+
+-public final class Ticket<T> implements Comparable<Ticket<?>> {
++public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system
private final TicketType<T> type;
private final int ticketLevel;
public final T key;
- private long createdTick;
+ // Paper start - rewrite chunk system
-+ public long removeDelay;
++ private long removeDelay;
- protected Ticket(TicketType<T> type, int level, T argument) {
-+ public Ticket(TicketType<T> type, int level, T argument, long removeDelay) {
++ @Override
++ public final long moonrise$getRemoveDelay() {
++ return this.removeDelay;
++ }
++
++ @Override
++ public final void moonrise$setRemoveDelay(final long removeDelay) {
+ this.removeDelay = removeDelay;
-+ // Paper end - rewrite chunk system
++ }
++ // Paper end - rewerite chunk system
++
++ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public
this.type = type;
this.ticketLevel = level;
this.key = argument;
-@@ -41,7 +44,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
@Override
public String toString() {
@@ -19177,7 +25821,7 @@ index eba83b085435150e5954fd5d41dda9ce1d0601ad..e97329f867de2acbdd666925ba5d2aaf
}
public TicketType<T> getType() {
-@@ -53,11 +56,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
+@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
}
protected void setCreatedTick(long tickCreated) {
@@ -19191,178 +25835,82 @@ index eba83b085435150e5954fd5d41dda9ce1d0601ad..e97329f867de2acbdd666925ba5d2aaf
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
}
-diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
-index 6051e5f272838ef23276a90e21c2fc821ca155d1..658e63ebde81dc14c8ab5850fb246dc0aab25dea 100644
---- a/src/main/java/net/minecraft/server/level/TicketType.java
-+++ b/src/main/java/net/minecraft/server/level/TicketType.java
-@@ -8,6 +8,7 @@ import net.minecraft.world.level.ChunkPos;
-
- public class TicketType<T> {
- public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
-+ public static final TicketType<Long> ASYNC_LOAD = create("async_load", Long::compareTo); // Paper
-
- private final String name;
- private final Comparator<T> comparator;
-@@ -27,6 +28,15 @@ public class TicketType<T> {
- public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
- public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
- public static final TicketType<Long> CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting
-+ // Paper start - rewrite chunk system
-+ public static final TicketType<Long> CHUNK_LOAD = create("chunk_load", Long::compareTo);
-+ public static final TicketType<Long> STATUS_UPGRADE = create("status_upgrade", Long::compareTo);
-+ public static final TicketType<Long> ENTITY_LOAD = create("entity_load", Long::compareTo);
-+ public static final TicketType<Long> POI_LOAD = create("poi_load", Long::compareTo);
-+ public static final TicketType<Unit> UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
-+ public static final TicketType<Long> NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo);
-+ public static final TicketType<ChunkPos> DELAY_UNLOAD = create("delay_unload", Comparator.comparingLong(ChunkPos::toLong), 1);
-+ // Paper end - rewrite chunk system
-
- public static <T> TicketType<T> create(String name, Comparator<T> argumentComparator) {
- return new TicketType<>(name, argumentComparator, 0L);
diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-index abbd4140cb4478a34a5185d8555f83d96c04d468..a6c31a558794a6e626e83176a1cbe78b6bd90f6e 100644
+index b26a4a38144ec1b171db911bbf949b53ed35708f..5a8a33638ceb1d980ffc3e6dd86e7eb11dfd9375 100644
--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-@@ -554,4 +554,21 @@ public class WorldGenRegion implements WorldGenLevel {
- public long nextSubTickCount() {
- return this.subTickCount.getAndIncrement();
- }
-+
-+ // Paper start
-+ // No-op, this class doesn't provide entity access
+@@ -85,6 +85,36 @@ public class WorldGenRegion implements WorldGenLevel {
+ private final AtomicLong subTickCount = new AtomicLong();
+ private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random");
+
++ // Paper start - rewrite chunk system
++ /**
++ * During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla
++ * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate.
++ * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate
++ * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished
++ * lighting.
++ */
+ @Override
-+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
-+ return Collections.emptyList();
++ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
++ if (!chunk.isLightCorrect()) {
++ return 0;
++ }
++ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
+ }
+
++ /**
++ * See above
++ */
+ @Override
-+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
-+
-+ @Override
-+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
++ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
++ if (!chunk.isLightCorrect()) {
++ return 0;
++ }
++ return this.getLightEngine().getRawBrightness(blockPos, subtract);
++ }
++ // Paper end - rewrite chunk system
+
-+ @Override
-+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, AABB box, List<? super T> into, Predicate<? super T> predicate) {}
-+ // Paper end
- }
+ public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) {
+ this.generatingStep = generationStep;
+ this.cache = chunks;
diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
-index c502d9b85eb68b277ae17dfea34e0475f0156647..27d0f1ed58948039004f8f1eba2f7f9609fdeec0 100644
+index cdd66e6ce96e2613afe7f06ca8da3cfaa6704b2d..32634e45ac8433648e49e47e20081e15ad41ff15 100644
--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
-@@ -43,16 +43,23 @@ public class PlayerChunkSender {
-
- public void dropChunk(ServerPlayer player, ChunkPos pos) {
- if (!this.pendingChunks.remove(pos.toLong()) && player.isAlive()) {
-+ // Paper start - rewrite player chunk loader
-+ dropChunkStatic(player, pos);
-+ }
-+ }
-+ public static void dropChunkStatic(ServerPlayer player, ChunkPos pos) {
-+ player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(pos.toLong()).removePlayer(player);
- player.connection.send(new ClientboundForgetLevelChunkPacket(pos));
- // Paper start - PlayerChunkUnloadEvent
- if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) {
- new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(pos.longKey), player.getBukkitEntity()).callEvent();
- }
- // Paper end - PlayerChunkUnloadEvent
-- }
- }
-+ // Paper end - rewrite player chunk loader
-
- public void sendNextChunks(ServerPlayer player) {
-+ if (true) return; // Paper - rewrite player chunk loader
- if (this.unacknowledgedBatches < this.maxUnacknowledgedBatches) {
- float f = Math.max(1.0F, this.desiredChunksPerTick);
- this.batchQuota = Math.min(this.batchQuota + this.desiredChunksPerTick, f);
-@@ -78,7 +85,8 @@ public class PlayerChunkSender {
+@@ -78,7 +78,7 @@ public class PlayerChunkSender {
}
}
- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
-+ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - rewrite chunk loader - public
-+ handler.player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(chunk.getPos().toLong()).addPlayer(handler.player);
++ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - public
handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
// Paper start - PlayerChunkLoadEvent
if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-@@ -118,6 +126,7 @@ public class PlayerChunkSender {
- }
-
- public void onChunkBatchReceivedByClient(float desiredBatchSize) {
-+ if (true) return; // Paper - rewrite player chunk loader
- this.unacknowledgedBatches--;
- this.desiredChunksPerTick = Double.isNaN((double)desiredBatchSize) ? 0.01F : Mth.clamp(desiredBatchSize, 0.01F, 64.0F);
- if (this.unacknowledgedBatches == 0) {
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0aa28caa1254137c0bae8e213bd08c9a654f5335..c4b4e5f5c9366b241686e881cda34568a57b4877 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -296,7 +296,7 @@ public abstract class PlayerList {
- boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING);
-
- // Spigot - view distance
-- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile()));
-+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management
- player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
- playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
- playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
-@@ -943,8 +943,8 @@ public abstract class PlayerList {
- LevelData worlddata = worldserver2.getLevelData();
-
- entityplayer1.connection.send(new ClientboundRespawnPacket(entityplayer1.createCommonSpawnInfo(worldserver2), (byte) i));
-- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot
-- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot
-+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management
-+ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management
- entityplayer1.connection.teleport(CraftLocation.toBukkit(entityplayer1.position(), worldserver2.getWorld(), entityplayer1.getYRot(), entityplayer1.getXRot())); // CraftBukkit
- entityplayer1.connection.send(new ClientboundSetDefaultSpawnPositionPacket(worldserver1.getSharedSpawnPos(), worldserver1.getSharedSpawnAngle()));
- entityplayer1.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
-@@ -1496,7 +1496,7 @@ public abstract class PlayerList {
-
- public void setViewDistance(int viewDistance) {
- this.viewDistance = viewDistance;
-- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance));
-+ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance
- Iterator iterator = this.server.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
-@@ -1511,7 +1511,7 @@ public abstract class PlayerList {
-
- public void setSimulationDistance(int simulationDistance) {
- this.simulationDistance = simulationDistance;
-- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance));
-+ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader
- Iterator iterator = this.server.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
-index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa1701293a 100644
+index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644
--- a/src/main/java/net/minecraft/util/SortedArraySet.java
+++ b/src/main/java/net/minecraft/util/SortedArraySet.java
-@@ -14,6 +14,14 @@ public class SortedArraySet<T> extends AbstractSet<T> {
+@@ -8,12 +8,89 @@ import java.util.Iterator;
+ import java.util.NoSuchElementException;
+ import javax.annotation.Nullable;
+
+-public class SortedArraySet<T> extends AbstractSet<T> {
++public class SortedArraySet<T> extends AbstractSet<T> implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet<T> { // Paper - rewrite chunk system
+ private static final int DEFAULT_INITIAL_CAPACITY = 10;
+ private final Comparator<T> comparator;
T[] contents;
int size;
+ // Paper start - rewrite chunk system
-+ public SortedArraySet(final SortedArraySet<T> other) {
-+ this.comparator = other.comparator;
-+ this.size = other.size;
-+ this.contents = Arrays.copyOf(other.contents, this.size);
-+ }
-+ // Paper end - rewrite chunk system
-+
- private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
- this.comparator = comparator;
- if (initialCapacity < 0) {
-@@ -22,6 +30,41 @@ public class SortedArraySet<T> extends AbstractSet<T> {
- this.contents = (T[])castRawArray(new Object[initialCapacity]);
- }
- }
-+ // Paper start - optimise removeIf
+ @Override
-+ public boolean removeIf(java.util.function.Predicate<? super T> filter) {
++ public final boolean removeIf(final java.util.function.Predicate<? super T> filter) {
+ // prev. impl used an iterator, which could be n^2 and creates garbage
-+ int i = 0, len = this.size;
-+ T[] backingArray = this.contents;
++ int i = 0;
++ final int len = this.size;
++ final T[] backingArray = this.contents;
+
+ for (;;) {
+ if (i >= len) {
@@ -19380,7 +25928,7 @@ index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa
+ int lastIndex = i; // this is where new elements are shifted to
+
+ for (; i < len; ++i) {
-+ T curr = backingArray[i];
++ final T curr = backingArray[i];
+ if (!filter.test(curr)) { // if test throws we're screwed
+ backingArray[lastIndex++] = curr;
+ }
@@ -19391,28 +25939,22 @@ index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa
+ this.size = lastIndex;
+ return true;
+ }
-+ // Paper end - optimise removeIf
-
- public static <T extends Comparable<T>> SortedArraySet<T> create() {
- return create(10);
-@@ -110,6 +153,31 @@ public class SortedArraySet<T> extends AbstractSet<T> {
- }
- }
-
-+ // Paper start - rewrite chunk system
-+ public T replace(T object) {
-+ int i = this.findIndex(object);
-+ if (i >= 0) {
-+ T old = this.contents[i];
-+ this.contents[i] = object;
++
++ @Override
++ public final T moonrise$replace(final T object) {
++ final int index = this.findIndex(object);
++ if (index >= 0) {
++ final T old = this.contents[index];
++ this.contents[index] = object;
+ return old;
+ } else {
-+ this.addInternal(object, getInsertionPosition(i));
++ this.addInternal(object, getInsertionPosition(index));
+ return object;
+ }
+ }
+
-+ public T removeAndGet(T object) {
++ @Override
++ public final T moonrise$removeAndGet(final T object) {
+ int i = this.findIndex(object);
+ if (i >= 0) {
+ final T ret = this.contents[i];
@@ -19422,140 +25964,139 @@ index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa
+ return null;
+ }
+ }
++
++ @Override
++ public final SortedArraySet<T> moonrise$copy() {
++ final SortedArraySet<T> ret = SortedArraySet.create(this.comparator, 0);
++
++ ret.size = this.size;
++ ret.contents = Arrays.copyOf(this.contents, this.size);
++
++ return ret;
++ }
++
++ @Override
++ public Object[] moonrise$copyBackingArray() {
++ return this.contents.clone();
++ }
+ // Paper end - rewrite chunk system
+
- @Override
- public boolean remove(Object object) {
- int i = this.findIndex((T)object);
-diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-index 0382b6597a130d746f8954a93a756a9d1ac81d50..ffbb3bf9ff3fc968ef69d4f889b0baf7e8ab691b 100644
---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
-@@ -227,7 +227,13 @@ public class WorldUpgrader {
- this.previousWriteFuture.join();
- }
-
-+ // Paper start - async chunk io
-+ try {
- this.previousWriteFuture = storage.write(chunkPos, nbttagcompound1);
-+ } catch (final IOException e) {
-+ com.destroystokyo.paper.util.SneakyThrow.sneaky(e);
-+ }
-+ // Paper end - async chunk io
- return true;
- }
- }
+ private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
+ this.comparator = comparator;
+ if (initialCapacity < 0) {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 62363c09111aaa31220fb260940744c097af7b3c..ff497f0e80889508dd8c183b48cd33bc7831ba6c 100644
+index 8779d54c816bb97ccdeb268d1929f693d322ee14..bf60b1aba3019996f53a3cf051d2a603cb7b8404 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -481,6 +481,58 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -167,7 +167,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
+ import org.bukkit.plugin.PluginManager;
+ // CraftBukkit end
+
+-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder {
++public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity { // Paper - rewrite chunk system
+
+ // CraftBukkit start
+ private static final int CURRENT_LEVEL = 2;
+@@ -456,6 +456,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ return this.dimensions.makeBoundingBox(x, y, z);
}
// Paper end
-
-+ // Paper start
-+ /**
-+ * Overriding this field will cause memory leaks.
-+ */
-+ private final boolean hardCollides;
++ // Paper start - rewrite chunk system
++ private final boolean isHardColliding = this.moonrise$isHardCollidingUncached();
++ private net.minecraft.server.level.FullChunkStatus chunkStatus;
++ private int sectionX = Integer.MIN_VALUE;
++ private int sectionY = Integer.MIN_VALUE;
++ private int sectionZ = Integer.MIN_VALUE;
++ private boolean updatingSectionStatus;
+
-+ private static final java.util.Map<Class<? extends Entity>, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>());
-+ {
-+ /* // Goodbye, broken on reobf...
-+ Boolean hardCollides = cachedOverrides.get(this.getClass());
-+ if (hardCollides == null) {
-+ try {
-+ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class);
-+ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith");
-+ if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod)
-+ || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) {
-+ hardCollides = Boolean.TRUE;
-+ } else {
-+ hardCollides = Boolean.FALSE;
-+ }
-+ cachedOverrides.put(this.getClass(), hardCollides);
-+ }
-+ catch (ThreadDeath thr) { throw thr; }
-+ catch (Throwable thr) {
-+ // shouldn't happen, just explode
-+ throw new RuntimeException(thr);
-+ }
-+ } */
-+ this.hardCollides = this instanceof Boat
-+ || this instanceof net.minecraft.world.entity.monster.Shulker
-+ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart
-+ || this.shouldHardCollide();
++ @Override
++ public final boolean moonrise$isHardColliding() {
++ return this.isHardColliding;
+ }
+
-+ // plugins can override
-+ protected boolean shouldHardCollide() {
-+ return false;
++ @Override
++ public final net.minecraft.server.level.FullChunkStatus moonrise$getChunkStatus() {
++ return this.chunkStatus;
+ }
+
-+ public final boolean hardCollides() {
-+ return this.hardCollides;
++ @Override
++ public final void moonrise$setChunkStatus(final net.minecraft.server.level.FullChunkStatus status) {
++ this.chunkStatus = status;
+ }
+
-+ public net.minecraft.server.level.FullChunkStatus chunkStatus;
++ @Override
++ public final int moonrise$getSectionX() {
++ return this.sectionX;
++ }
+
-+ public int sectionX = Integer.MIN_VALUE;
-+ public int sectionY = Integer.MIN_VALUE;
-+ public int sectionZ = Integer.MIN_VALUE;
++ @Override
++ public final void moonrise$setSectionX(final int x) {
++ this.sectionX = x;
++ }
+
-+ public boolean updatingSectionStatus = false;
-+ // Paper end
++ @Override
++ public final int moonrise$getSectionY() {
++ return this.sectionY;
++ }
+
- public Entity(EntityType<?> type, Level world) {
- this.id = Entity.ENTITY_COUNTER.incrementAndGet();
- this.passengers = ImmutableList.of();
-@@ -2607,11 +2659,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return InteractionResult.PASS;
- }
-
-- public boolean canCollideWith(Entity other) {
-+ public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override
- return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other);
- }
-
-- public boolean canBeCollidedWith() {
-+ public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override
- return false;
- }
-
-@@ -4042,6 +4094,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }).count();
- }
-
-+ // Paper start - rewrite chunk system
-+ public boolean hasAnyPlayerPassengers() {
-+ // copied from below
-+ if (this.passengers.isEmpty()) { return false; }
++ @Override
++ public final void moonrise$setSectionY(final int y) {
++ this.sectionY = y;
++ }
++
++ @Override
++ public final int moonrise$getSectionZ() {
++ return this.sectionZ;
++ }
++
++ @Override
++ public final void moonrise$setSectionZ(final int z) {
++ this.sectionZ = z;
++ }
++
++ @Override
++ public final boolean moonrise$isUpdatingSectionStatus() {
++ return this.updatingSectionStatus;
++ }
++
++ @Override
++ public final void moonrise$setUpdatingSectionStatus(final boolean to) {
++ this.updatingSectionStatus = to;
++ }
++
++ @Override
++ public final boolean moonrise$hasAnyPlayerPassengers() {
++ if (this.passengers.isEmpty()) {
++ return false;
++ }
+ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player);
+ }
+ // Paper end - rewrite chunk system
- public boolean hasExactlyOnePlayerPassenger() {
- if (this.passengers.isEmpty()) { return false; } // Paper - Optimize indirect passenger iteration
- return this.countPlayerPassengers() == 1;
-@@ -4392,6 +4451,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return;
- }
- // Paper end - Block invalid positions and bounding box
+
+ public Entity(EntityType<?> type, Level world) {
+ this.id = Entity.ENTITY_COUNTER.incrementAndGet();
+@@ -4408,6 +4479,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ this.setPosRaw(x, y, z, false);
+ }
+ public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
+ // Paper start - rewrite chunk system
+ if (this.updatingSectionStatus) {
-+ LOGGER.error("Refusing to update position for entity {} to position {} since it is processing a section status update", this, new Vec3(x, y, z), new Throwable());
++ LOGGER.error(
++ "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z)
++ + " since it is processing a section status update", new Throwable()
++ );
+ return;
+ }
+ // Paper end - rewrite chunk system
- // Paper start - Fix MC-4
- if (this instanceof ItemEntity) {
- if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) {
-@@ -4519,6 +4584,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ if (!checkPosition(this, x, y, z)) {
+ return;
+ }
+@@ -4539,6 +4619,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
+ // Paper start - rewrite chunk system
-+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot remove entity off-main");
-+ if (!((ServerLevel)this.level).getEntityLookup().canRemoveEntity(this)) {
++ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) {
+ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable());
+ return;
+ }
@@ -19563,143 +26104,86 @@ index 62363c09111aaa31220fb260940744c097af7b3c..ff497f0e80889508dd8c183b48cd33bc
CraftEventFactory.callEntityRemoveEvent(this, cause);
// CraftBukkit end
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
-@@ -4530,7 +4602,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4550,7 +4636,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.stopRiding();
}
- this.getPassengers().forEach(Entity::stopRiding);
-+ if (entity_removalreason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload)
++ if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
this.levelCallback.onRemove(entity_removalreason);
// Paper start - Folia schedulers
if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
-@@ -4561,7 +4633,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4581,7 +4667,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public boolean shouldBeSaved() {
- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
-+ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasAnyPlayerPassengers()); // Paper - rewrite chunk system - it should check if the entity has ANY player passengers
++ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397bef51f94d 100644
+index fb63036d26d2b5370472b741b23bebd71e247463..274ddf479d38495d84838f9cd73c13d2841c3b44 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -40,20 +40,40 @@ import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
- public class PoiManager extends SectionStorage<PoiSection> {
+@@ -38,12 +38,153 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
+ import net.minecraft.world.level.chunk.storage.SectionStorage;
+ import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
+
+-public class PoiManager extends SectionStorage<PoiSection> {
++public class PoiManager extends SectionStorage<PoiSection> implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system
public static final int MAX_VILLAGE_DISTANCE = 6;
public static final int VILLAGE_SECTION_SIZE = 1;
-- private final PoiManager.DistanceTracker distanceTracker;
-- private final LongSet loadedChunks = new LongOpenHashSet();
+ private final PoiManager.DistanceTracker distanceTracker;
+ private final LongSet loadedChunks = new LongOpenHashSet();
+
+ // Paper start - rewrite chunk system
-+ // the vanilla tracker needs to be replaced because it does not support level removes
-+ public final net.minecraft.server.level.ServerLevel world;
-+ private final io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D();
-+ static final int POI_DATA_SOURCE = 7;
-+ public static int convertBetweenLevels(final int level) {
++ private final net.minecraft.server.level.ServerLevel world;
++
++ // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes
++ // to support poi unloading
++ private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D();
++
++ private static final int POI_DATA_SOURCE = 7;
++
++ private static int convertBetweenLevels(final int level) {
+ return POI_DATA_SOURCE - level;
+ }
+
-+ protected void updateDistanceTracking(long section) {
++ private void updateDistanceTracking(long section) {
+ if (this.isVillageCenter(section)) {
+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
+ } else {
+ this.villageDistanceTracker.removeSource(section);
+ }
+ }
-+ // Paper end - rewrite chunk system
-
- public PoiManager(
- RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world
- ) {
- super(
-+ // Paper start
-+ storageKey,
-+ directory,
-+ dsync,
-+ // Paper end
- new SimpleRegionStorage(storageKey, directory, dataFixer, dsync, DataFixTypes.POI_CHUNK),
- PoiSection::codec,
- PoiSection::new,
- registryManager,
- world
- );
-- this.distanceTracker = new PoiManager.DistanceTracker();
-+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
- }
-
- public void add(BlockPos pos, Holder<PoiType> type) {
-@@ -187,8 +207,8 @@ public class PoiManager extends SectionStorage<PoiSection> {
- }
-
- public int sectionsToVillage(SectionPos pos) {
-- this.distanceTracker.runAllUpdates();
-- return this.distanceTracker.getLevel(pos.asLong());
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util
-+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util
- }
-
- boolean isVillageCenter(long pos) {
-@@ -202,20 +222,117 @@ public class PoiManager extends SectionStorage<PoiSection> {
-
- @Override
- public void tick(BooleanSupplier shouldKeepTicking) {
-- super.tick(shouldKeepTicking);
-- this.distanceTracker.runAllUpdates();
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
- }
-
- @Override
-- protected void setDirty(long pos) {
-- super.setDirty(pos);
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ public void setDirty(long pos) {
-+ // Paper start - rewrite chunk system
-+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos);
-+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos);
-+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager;
-+ io.papermc.paper.chunk.system.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
-+ if (chunk != null) {
-+ chunk.setDirty(true);
-+ }
-+ this.updateDistanceTracking(pos);
-+ // Paper end - rewrite chunk system
- }
-
- @Override
- protected void onSectionLoad(long pos) {
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ this.updateDistanceTracking(pos); // Paper - move to new distance tracking util
-+ }
+
-+ // Paper start - rewrite chunk system
+ @Override
-+ public Optional<PoiSection> get(long pos) {
-+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos);
-+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos);
-+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos);
++ public Optional<PoiSection> get(final long pos) {
++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
++ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
-+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager;
-+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+
+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY);
+ }
+
+ @Override
-+ public Optional<PoiSection> getOrLoad(long pos) {
-+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos);
-+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos);
-+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos);
++ public Optional<PoiSection> getOrLoad(final long pos) {
++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
++ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
-+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager;
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+
-+ if (chunkY >= io.papermc.paper.util.WorldUtil.getMinSection(this.world) &&
-+ chunkY <= io.papermc.paper.util.WorldUtil.getMaxSection(this.world)) {
-+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
++ if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) {
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+ if (ret != null) {
+ return ret.getSectionForVanilla(chunkY);
+ } else {
@@ -19711,16 +26195,16 @@ index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397b
+ }
+
+ @Override
-+ protected PoiSection getOrCreate(long pos) {
-+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos);
-+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos);
-+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos);
++ protected PoiSection getOrCreate(final long pos) {
++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
++ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
-+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager;
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+
-+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+ if (ret != null) {
+ return ret.getOrCreateSection(chunkY);
+ } else {
@@ -19728,43 +26212,128 @@ index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397b
+ }
+ }
+
-+ public void onUnload(long coordinate) { // Paper - rewrite chunk system
-+ int chunkX = io.papermc.paper.util.MCUtil.getCoordinateX(coordinate);
-+ int chunkZ = io.papermc.paper.util.MCUtil.getCoordinateZ(coordinate);
++ @Override
++ public final net.minecraft.server.level.ServerLevel moonrise$getWorld() {
++ return this.world;
++ }
++
++ @Override
++ public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system
++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate);
++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate);
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main");
+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-+ long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
++ final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
+ this.updateDistanceTracking(sectionPos);
+ }
+ }
+
-+ public void loadInPoiChunk(io.papermc.paper.chunk.system.poi.PoiChunk poiChunk) {
-+ int chunkX = poiChunk.chunkX;
-+ int chunkZ = poiChunk.chunkZ;
++ @Override
++ public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) {
++ final int chunkX = poiChunk.chunkX;
++ final int chunkZ = poiChunk.chunkZ;
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main");
+ for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) {
-+ PoiSection section = poiChunk.getSection(sectionY);
-+ if (section != null && !section.isEmpty()) {
++ final PoiSection section = poiChunk.getSection(sectionY);
++ if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ));
+ }
+ }
+ }
+
-+ public void checkConsistency(net.minecraft.world.level.chunk.ChunkAccess chunk) {
-+ int chunkX = chunk.getPos().x;
-+ int chunkZ = chunk.getPos().z;
-+ int minY = io.papermc.paper.util.WorldUtil.getMinSection(chunk);
-+ int maxY = io.papermc.paper.util.WorldUtil.getMaxSection(chunk);
-+ LevelChunkSection[] sections = chunk.getSections();
++ @Override
++ public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) {
++ final int chunkX = chunk.getPos().x;
++ final int chunkZ = chunk.getPos().z;
++
++ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk);
++ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk);
++ final LevelChunkSection[] sections = chunk.getSections();
+ for (int section = minY; section <= maxY; ++section) {
+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]);
+ }
- }
++ }
++
++ @Override
++ public final void moonrise$close() throws java.io.IOException {}
++
++ @Override
++ public final net.minecraft.nbt.CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws java.io.IOException {
++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
++ return ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
++ this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA,
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
++ );
++ }
++ return this.moonrise$getRegionStorage().read(new ChunkPos(chunkX, chunkZ));
++ }
++
++ @Override
++ public final void moonrise$write(final int chunkX, final int chunkZ, final net.minecraft.nbt.CompoundTag data) throws java.io.IOException {
++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA);
++ return;
++ }
++ this.moonrise$getRegionStorage().write(new ChunkPos(chunkX, chunkZ), data);
++ }
+ // Paper end - rewrite chunk system
++
+ public PoiManager(
+ RegionStorageInfo storageKey,
+ Path directory,
+@@ -62,6 +203,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
+ world
+ );
+ this.distanceTracker = new PoiManager.DistanceTracker();
++ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
+ }
+
+ public void add(BlockPos pos, Holder<PoiType> type) {
+@@ -195,8 +337,8 @@ public class PoiManager extends SectionStorage<PoiSection> {
+ }
+
+ public int sectionsToVillage(SectionPos pos) {
+- this.distanceTracker.runAllUpdates();
+- return this.distanceTracker.getLevel(pos.asLong());
++ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
++ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system
+ }
+
+ boolean isVillageCenter(long pos) {
+@@ -210,19 +352,26 @@ public class PoiManager extends SectionStorage<PoiSection> {
+
+ @Override
+ public void tick(BooleanSupplier shouldKeepTicking) {
+- super.tick(shouldKeepTicking);
+- this.distanceTracker.runAllUpdates();
++ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
+ }
+
+ @Override
+- protected void setDirty(long pos) {
+- super.setDirty(pos);
+- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
++ public void setDirty(long pos) { // Paper - public
++ // Paper start - rewrite chunk system
++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
++ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
++ if (chunk != null) {
++ chunk.setDirty(true);
++ }
++ this.updateDistanceTracking(pos);
++ // Paper end - rewrite chunk system
+ }
+
+ @Override
+ protected void onSectionLoad(long pos) {
+- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
++ this.updateDistanceTracking(pos); // Paper - rewrite chunk system
+ }
public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
- Util.ifElse(this.getOrLoad(sectionPos.asLong()), poiSet -> poiSet.refresh(populator -> {
-@@ -251,7 +368,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
+@@ -259,7 +408,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
.map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong())))
.filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false))
.map(pair -> pair.getFirst().chunk())
@@ -19773,116 +26342,157 @@ index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397b
.forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
}
-@@ -265,7 +382,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
-
- @Override
- protected int getLevelFromSource(long id) {
-- return PoiManager.this.isVillageCenter(id) ? 0 : 7;
-+ return PoiManager.this.isVillageCenter(id) ? 0 : 7; // Paper - rewrite chunk system - diff on change, this specifies the source level to use for distance tracking
- }
+diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+index 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..a6c0e89cb645693034f8e90ac2de8f2da457453c 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+@@ -23,7 +23,7 @@ import net.minecraft.core.SectionPos;
+ import net.minecraft.util.VisibleForDebug;
+ import org.slf4j.Logger;
- @Override
-@@ -287,6 +404,35 @@ public class PoiManager extends SectionStorage<PoiSection> {
- }
+-public class PoiSection {
++public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
+ private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
+@@ -42,6 +42,20 @@ public class PoiSection {
+ .orElseGet(Util.prefix("Failed to read POI section: ", LOGGER::error), () -> new PoiSection(updateListener, false, ImmutableList.of()));
}
-+ // Paper start - Asynchronous chunk io
-+ @javax.annotation.Nullable
++ // Paper start - rewrite chunk system
++ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);;
++
+ @Override
-+ public net.minecraft.nbt.CompoundTag read(ChunkPos chunkcoordintpair) throws java.io.IOException {
-+ // Paper start - rewrite chunk system
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
-+ );
-+ }
-+ // Paper end - rewrite chunk system
-+ return super.read(chunkcoordintpair);
++ public final boolean moonrise$isEmpty() {
++ return this.isValid && this.records.isEmpty() && this.byType.isEmpty();
+ }
+
+ @Override
-+ public void write(ChunkPos chunkcoordintpair, net.minecraft.nbt.CompoundTag nbttagcompound) throws java.io.IOException {
-+ // Paper start - rewrite chunk system
-+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave(
-+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound,
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA);
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
-+ super.write(chunkcoordintpair, nbttagcompound);
++ public final Optional<PoiSection> moonrise$asOptional() {
++ return this.noAllocOptional;
+ }
-+ // Paper end
++ // Paper end - rewrite chunk system
+
- public static enum Occupancy {
- HAS_SPACE(PoiRecord::hasSpace),
- IS_OCCUPIED(PoiRecord::isOccupied),
-diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-index 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..5b7deae326228e482b218aeebd857a59b7434eaf 100644
---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-@@ -29,6 +29,7 @@ public class PoiSection {
- private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
- private final Runnable setDirty;
- private boolean isValid;
-+ public final Optional<PoiSection> noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system
-
- public static Codec<PoiSection> codec(Runnable updateListener) {
- return RecordCodecBuilder.<PoiSection>create(
-@@ -46,6 +47,12 @@ public class PoiSection {
+ public PoiSection(Runnable updateListener) {
this(updateListener, true, ImmutableList.of());
}
-
-+ // Paper start - isEmpty
-+ public boolean isEmpty() {
-+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty();
-+ }
-+ // Paper end
-+
- private PoiSection(Runnable updateListener, boolean valid, List<PoiRecord> pois) {
- this.setDirty = updateListener;
- this.isValid = valid;
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
-index bd20bea7f76a7307f1698fb2dfef37125032d166..9a28912f52824acdc80a62243b136e6f365bf567 100644
+index bd20bea7f76a7307f1698fb2dfef37125032d166..70c2017400168d4fef3c14462798edcfed58d4bf 100644
--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
-@@ -19,6 +19,18 @@ import net.minecraft.world.phys.shapes.Shapes;
+@@ -18,7 +18,7 @@ import net.minecraft.world.phys.shapes.BooleanOp;
+ import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
- public interface EntityGetter {
-+
-+ // Paper start
-+ List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate);
-+
-+ void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into);
-+
-+ void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into);
-+
-+ <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, final AABB box, List<? super T> into,
-+ Predicate<? super T> predicate);
-+ // Paper end
-+
+-public interface EntityGetter {
++public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
<T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate);
+@@ -33,6 +33,13 @@ public interface EntityGetter {
+ return this.getEntities(except, box, EntitySelector.NO_SPECTATORS);
+ }
+
++ // Paper start - rewrite chunk system
++ @Override
++ default List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
++ return this.getEntities(entity, box, predicate);
++ }
++ // Paper end - rewrite chunk system
++
+ default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
+ if (shape.isEmpty()) {
+ return true;
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafeffc6f82c 100644
+index 6f822e9487bef5b9766d5ae86ebbd687e4eadc42..77c6613c39e3b266944e28cf2627483d9f32c511 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -547,6 +547,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -102,7 +102,7 @@ import org.bukkit.entity.SpawnCategory;
+ import org.bukkit.event.block.BlockPhysicsEvent;
+ // CraftBukkit end
+
+-public abstract class Level implements LevelAccessor, AutoCloseable {
++public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
+
+ public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
+ public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
+@@ -199,6 +199,58 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+
+ public abstract ResourceKey<LevelStem> getTypeKey();
- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
++ // Paper start - rewrite chunk system
++ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup;
++
++ @Override
++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() {
++ return this.entityLookup;
++ }
++
++ @Override
++ public void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) {
++ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) {
++ throw new IllegalStateException("Entity lookup already initialised");
++ }
++ this.entityLookup = entityLookup;
++ }
++
++ @Override
++ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) {
++ this.getProfiler().incrementCounter("getEntities");
++ final List<T> ret = new java.util.ArrayList<>();
++
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate);
++
++ return ret;
++ }
++
++ @Override
++ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
++ this.getProfiler().incrementCounter("getEntities");
++ final List<Entity> ret = new java.util.ArrayList<>();
++
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate);
++
++ return ret;
++ }
++
++ @Override
++ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
++ return this.getChunkSource().getChunk(chunkX, chunkZ, false);
++ }
++
++ @Override
++ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
++ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false);
++ }
++
++ @Override
++ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) {
++ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false);
++ }
++ // Paper end - rewrite chunk system
++
+ protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config
+ this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+@@ -281,6 +333,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings
+ this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
+ this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
++ this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system
+ }
+
+ // Paper start - Cancel hit for vanished players
+@@ -551,7 +604,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
+ }
+
+- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
++ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
-+ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance
-+ // if copied from above
-+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management
-+ ((ServerLevel)this).getChunkSource().blockChanged(blockposition);
-+ // Paper end - per player view distance
}
- if ((i & 1) != 0) {
-@@ -941,7 +946,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -951,7 +1004,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
}
// Paper end - Perf: Optimize capturedTileEntities lookup
// CraftBukkit end
@@ -19891,16 +26501,19 @@ index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafe
}
public void setBlockEntity(BlockEntity blockEntity) {
-@@ -1032,26 +1037,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -1041,28 +1094,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ @Override
public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
this.getProfiler().incrementCounter("getEntities");
- List<Entity> list = Lists.newArrayList();
+- List<Entity> list = Lists.newArrayList();
-
- this.getEntities().get(box, (entity1) -> {
- if (entity1 != except && predicate.test(entity1)) {
- list.add(entity1);
- }
--
++ // Paper start - rewrite chunk system
++ final List<Entity> ret = new java.util.ArrayList<>();
+
- if (entity1 instanceof EnderDragon) {
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
- int i = aentitycomplexpart.length;
@@ -19913,15 +26526,24 @@ index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafe
- }
- }
- }
--
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
+
- });
-+ ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call
- return list;
+- return list;
++ return ret;
++ // Paper end - rewrite chunk system
}
-@@ -1069,33 +1055,23 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ @Override
+@@ -1077,36 +1115,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
+ }
- public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
+- public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
++ // Paper start - rewrite chunk system
++ public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
++ final AABB boundingBox, final Predicate<? super T> predicate,
++ final List<? super T> into, final int maxCount) {
this.getProfiler().incrementCounter("getEntities");
- this.getEntities().get(filter, box, (entity) -> {
- if (predicate.test(entity)) {
@@ -19929,487 +26551,777 @@ index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafe
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
-- }
--
++
++ if (entityTypeTest instanceof net.minecraft.world.entity.EntityType<T> byType) {
++ if (maxCount != Integer.MAX_VALUE) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount);
++ return;
++ } else {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate);
++ return;
+ }
++ }
+
- if (entity instanceof EnderDragon entityenderdragon) {
- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities();
- int j = aentitycomplexpart.length;
--
++ if (entityTypeTest == null) {
++ if (maxCount != Integer.MAX_VALUE) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount);
++ return;
++ } else {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate);
++ return;
++ }
++ }
+
- for (int k = 0; k < j; ++k) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
--
++ final Class<? extends Entity> base = entityTypeTest.getBaseClass();
+
- if (t0 != null && predicate.test(t0)) {
- result.add(t0);
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
- }
-- }
-+ // Paper start - optimise this call
-+ //TODO use limit
-+ if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) {
-+ ((ServerLevel) this).getEntityLookup().getEntities(entityTypeTest, box, result, predicate);
++ final Predicate<? super T> modifiedPredicate;
++ if (predicate == null) {
++ modifiedPredicate = (final T obj) -> {
++ return entityTypeTest.tryCast(obj) != null;
++ };
+ } else {
-+ Predicate<? super T> test = (obj) -> {
-+ return filter.tryCast(obj) != null;
++ modifiedPredicate = (final Entity obj) -> {
++ final T casted = entityTypeTest.tryCast(obj);
++ if (casted == null) {
++ return false;
+ }
++
++ return predicate.test(casted);
+ };
-+ predicate = predicate == null ? test : test.and((Predicate) predicate);
-+ Class base;
-+ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) {
-+ ((ServerLevel) this).getEntityLookup().getEntities((Entity) null, box, (List) result, (Predicate)predicate);
++ }
++
++ if (base == null || base == Entity.class) {
++ if (maxCount != Integer.MAX_VALUE) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
++ return;
+ } else {
-+ ((ServerLevel) this).getEntityLookup().getEntities(base, null, box, (List) result, (Predicate)predicate); // Paper - optimise this call
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate);
++ return;
}
--
++ } else {
++ if (maxCount != Integer.MAX_VALUE) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
++ return;
++ } else {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate);
++ return;
++ }
++ }
++ }
+
- return AbortableIterationConsumer.Continuation.CONTINUE;
- });
++ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
++ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ);
++ if (slices == null) {
++ return new org.bukkit.entity.Entity[0];
+ }
-+ // Paper end - optimise this call
++ return slices.getChunkEntities();
}
++ // Paper end - rewrite chunk system
@Nullable
-@@ -1385,4 +1361,45 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- }
+ public abstract Entity getEntity(int id);
+diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
+index a0ae26d6197e1069ca09982b4f8b706c55ae8491..1a4dc4b2561dbaf01246b4fb46266b1ac84008b8 100644
+--- a/src/main/java/net/minecraft/world/level/LevelReader.java
++++ b/src/main/java/net/minecraft/world/level/LevelReader.java
+@@ -22,7 +22,18 @@ import net.minecraft.world.level.dimension.DimensionType;
+ import net.minecraft.world.level.levelgen.Heightmap;
+ import net.minecraft.world.phys.AABB;
+
+-public interface LevelReader extends BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource {
++public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource { // Paper - rewrite chunk system
++
++ // Paper start - rewrite chunk system
++ @Override
++ public default ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
++ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
++ throw new IllegalArgumentException("Status: " + status.toString());
++ }
++ return ((LevelReader)this).getChunk(chunkX, chunkZ, status, true);
++ }
++ // Paper end - rewrite chunk system
++
+ @Nullable
+ ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+
+diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+index 6c4a339be29bb9c07b741a1ca12de2217c8687ba..a768b07dae4bf75b68e3bc1d3de4b68fc7d23842 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+@@ -762,7 +762,7 @@ public abstract class BlockBehaviour implements FeatureElement {
+ boolean test(BlockState state, BlockGetter world, BlockPos pos);
}
- // Paper end - notify observers even if grow failed
-+ // Paper start
-+ //protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - rewrite chunk system
+
+- public abstract static class BlockStateBase extends StateHolder<Block, BlockState> {
++ public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState { // Paper - rewrite chunk system
+
+ private final int lightEmission;
+ private final boolean useShapeForLightOcclusion;
+@@ -794,6 +794,21 @@ public abstract class BlockBehaviour implements FeatureElement {
+ private FluidState fluidState;
+ private boolean isRandomlyTicking;
+
++ // Paper start - rewrite chunk system
++ private int opacityIfCached;
++ private boolean isConditionallyFullOpaque;
+
-+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
-+ io.papermc.paper.world.ChunkEntitySlices slices = ((ServerLevel)this).getEntityLookup().getChunk(chunkX, chunkZ);
-+ if (slices == null) {
-+ return new org.bukkit.entity.Entity[0];
++ @Override
++ public final boolean starlight$isConditionallyFullOpaque() {
++ return this.isConditionallyFullOpaque;
+ }
-+ return slices.getChunkEntities();
++
++ @Override
++ public final int starlight$getOpacityIfCached() {
++ return this.opacityIfCached;
++ }
++ // Paper end - rewrite chunk system
++
+ protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
+ super(block, propertyMap, codec);
+ this.fluidState = Fluids.EMPTY.defaultFluidState();
+@@ -864,6 +879,10 @@ public abstract class BlockBehaviour implements FeatureElement {
+ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
+
+ this.legacySolid = this.calculateSolid();
++ // Paper start - rewrite chunk system
++ this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion;
++ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock;
++ // Paper end - rewrite chunk system
+ }
+
+ public Block getBlock() {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+index db4d95ce98eb1490d5306d1f74b282d27264871a..fb7bdf43fdc4d816b1c1f1f063bc170561c9544f 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -57,7 +57,7 @@ import net.minecraft.world.ticks.SerializableTickContainer;
+ import net.minecraft.world.ticks.TickContainerAccess;
+ import org.slf4j.Logger;
+
+-public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess {
++public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
+
+ public static final int NO_FILLED_SECTION = -1;
+ private static final Logger LOGGER = LogUtils.getLogger();
+@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+ @Nullable
+ protected BlendingData blendingData;
+ public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
+- protected ChunkSkyLightSources skyLightSources;
++ // Paper - rewrite chunk system
+ private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
+ private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
+ protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
+@@ -90,6 +90,53 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+ public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
+ // CraftBukkit end
+
++ // Paper start - rewrite chunk system
++ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles;
++ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles;
++ private volatile boolean[] skyEmptinessMap;
++ private volatile boolean[] blockEmptinessMap;
++
++ @Override
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
++ return this.blockNibbles;
+ }
+
+ @Override
-+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
-+ List<Entity> ret = new java.util.ArrayList<>();
-+ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, ret, predicate);
-+ return ret;
++ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
++ this.blockNibbles = nibbles;
+ }
+
+ @Override
-+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
-+ ((ServerLevel)this).getEntityLookup().getEntities(except, box, into, predicate);
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
++ return this.skyNibbles;
+ }
+
+ @Override
-+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
-+ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, into, predicate);
++ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
++ this.skyNibbles = nibbles;
+ }
+
+ @Override
-+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, final AABB box, List<? super T> into,
-+ Predicate<? super T> predicate) {
-+ ((ServerLevel)this).getEntityLookup().getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate);
++ public boolean[] starlight$getSkyEmptinessMap() {
++ return this.skyEmptinessMap;
+ }
+
+ @Override
-+ public <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box, Predicate<? super T> predicate) {
-+ List<T> ret = new java.util.ArrayList<>();
-+ ((ServerLevel)this).getEntityLookup().getEntities(entityClass, null, box, ret, predicate);
-+ return ret;
++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
++ this.skyEmptinessMap = emptinessMap;
+ }
-+ // Paper end
- }
-diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
-index a0ae26d6197e1069ca09982b4f8b706c55ae8491..32bfeb9aa87b43a9d2ce46dcc99dbd0ff355b412 100644
---- a/src/main/java/net/minecraft/world/level/LevelReader.java
-+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
-@@ -26,6 +26,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal
- @Nullable
- ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
-
-+ // Paper start - rewrite chunk system
-+ default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) {
-+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
-+ throw new IllegalArgumentException("Status: " + status.toString());
-+ }
-+ return this.getChunk(chunkX, chunkZ, status, true);
++
++ @Override
++ public boolean[] starlight$getBlockEmptinessMap() {
++ return this.blockEmptinessMap;
++ }
++
++ @Override
++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
++ this.blockEmptinessMap = emptinessMap;
+ }
+ // Paper end - rewrite chunk system
+
- @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
- @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
+ public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
+ this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
+ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
+@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+ this.inhabitedTime = inhabitedTime;
+ this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
+ this.blendingData = blendingData;
+- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
++ // Paper - rewrite chunk system
+ if (sectionArray != null) {
+ if (this.sections.length == sectionArray.length) {
+ System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
+@@ -111,6 +158,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+ ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
+ // CraftBukkit start
+ this.biomeRegistry = biomeRegistry;
++ // Paper start - rewrite chunk system
++ if (!((Object)this instanceof ImposterProtoChunk)) {
++ this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
++ this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
++ }
++ // Paper end - rewrite chunk system
+ }
+ public final Registry<Biome> biomeRegistry;
+ // CraftBukkit end
+@@ -514,12 +567,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+ }
+
+ public void initializeLightSources() {
+- this.skyLightSources.fillFrom(this);
++ // Paper - rewrite chunk system
+ }
+ @Override
+ public ChunkSkyLightSources getSkyLightSources() {
+- return this.skyLightSources;
++ return null; // Paper - rewrite chunk system
+ }
+
+ public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
-index c9cd18ce79a6ee7297a8fd14f4dbe712570b3ced..927bdebdb8ae01613f0cea074b3367bd7ffe9ab1 100644
+index 29697fad32dad3377eebc82d280ba48d3c1ad516..488938c32a48437721a71d294c77468f00c035b9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
-@@ -120,7 +120,7 @@ public abstract class ChunkGenerator {
+@@ -119,7 +119,7 @@ public abstract class ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler());
return chunk;
- }), Util.backgroundExecutor());
-+ }), executor); // Paper - run with supplied executor
++ }), Runnable::run); // Paper - rewrite chunk system
}
public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep);
-@@ -315,7 +315,7 @@ public abstract class ChunkGenerator {
+@@ -314,7 +314,7 @@ public abstract class ChunkGenerator {
return Pair.of(placement.getLocatePos(pos), holder);
}
- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
-+ ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
++ ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
} while (structurestart == null);
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-index bac191f92ea3735df19c68d5568c2c7962c8680f..5d94aee1303d9eca5f1fa9a2e033ad0d12909635 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -86,6 +86,7 @@ public class LevelChunk extends ChunkAccess {
- private final Int2ObjectMap<GameEventListenerRegistry> gameEventListenerRegistrySections;
- private final LevelChunkTicks<Block> blockTicks;
- private final LevelChunkTicks<Fluid> fluidTicks;
-+ public volatile FullChunkStatus chunkStatus = FullChunkStatus.INACCESSIBLE; // Paper - rewrite chunk system
+diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.level.material.FluidState;
+ import net.minecraft.world.level.material.Fluids;
- public LevelChunk(Level world, ChunkPos pos) {
- this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null);
-@@ -690,9 +691,26 @@ public class LevelChunk extends ChunkAccess {
+-public class EmptyLevelChunk extends LevelChunk {
++public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
+ private final Holder<Biome> biome;
+ public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) {
+@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
+ this.biome = biomeEntry;
}
-- // CraftBukkit start
-- public void loadCallback() {
-- // Paper start - neighbour cache
-+ // Paper start - new load callbacks
-+ private io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder;
-+ public io.papermc.paper.chunk.system.scheduling.NewChunkHolder getChunkHolder() {
-+ return this.chunkHolder;
++ // Paper start - rewrite chunk system
++ @Override
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
++ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
-+ public void setChunkHolder(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ if (chunkHolder == null) {
-+ throw new NullPointerException("Chunkholder cannot be null");
-+ }
-+ if (this.chunkHolder != null) {
-+ throw new IllegalStateException("Already have chunkholder: " + this.chunkHolder + ", cannot replace with " + chunkHolder);
-+ }
-+ this.chunkHolder = chunkHolder;
-+ this.playerChunk = chunkHolder.vanillaChunkHolder;
++ @Override
++ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
++
++ @Override
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
++ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
-+ /* Note: We skip the light neighbour chunk loading done for the vanilla full chunk */
-+ /* Starlight does not need these chunks for lighting purposes because of edge checks */
-+ public void pushChunkIntoLoadedMap() {
- int chunkX = this.chunkPos.x;
- int chunkZ = this.chunkPos.z;
- net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource();
-@@ -707,10 +725,55 @@ public class LevelChunk extends ChunkAccess {
- }
- }
- this.setNeighbourLoaded(0, 0, this);
-+ this.level.getChunkSource().addLoadedChunk(this);
-+ }
-+
-+ public void onChunkLoad(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ // figure out how this should interface with:
-+ // the entity chunk load event // -> moved to the FULL status
-+ // the chunk load event // -> stays here
-+ // any entity add to world events // -> in FULL status
-+ this.loadCallback();
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(this, chunkHolder.vanillaChunkHolder);
-+ }
-+
-+ public void onChunkUnload(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ // figure out how this should interface with:
-+ // the entity chunk load event // -> moved to chunk unload to disk (not written yet)
-+ // the chunk load event // -> stays here
-+ // any entity add to world events // -> goes into the unload logic, it will completely explode
-+ // etc later
-+ this.unloadCallback();
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this, chunkHolder.vanillaChunkHolder);
-+ }
-+
-+ public void onChunkTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ this.postProcessGeneration();
-+ this.level.startTickingChunk(this);
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(this, chunkHolder.vanillaChunkHolder);
-+ }
-+
-+ public void onChunkNotTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this, chunkHolder.vanillaChunkHolder);
-+ }
-+
-+ public void onChunkEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(this, chunkHolder.vanillaChunkHolder);
-+ }
-+
-+ public void onChunkNotEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this, chunkHolder.vanillaChunkHolder);
-+ }
-+ // Paper end - new load callbacks
-+
-+ // CraftBukkit start
-+ public void loadCallback() {
-+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
-+ // Paper - rewrite chunk system - move into separate callback
- this.loadedTicketLevel = true;
-- // Paper end - neighbour cache
-+ // Paper - rewrite chunk system - move into separate callback
- org.bukkit.Server server = this.level.getCraftServer();
-- this.level.getChunkSource().addLoadedChunk(this); // Paper
-+ // Paper - rewrite chunk system - move into separate callback
- if (server != null) {
- /*
- * If it's a new world, the first few chunks are generated inside
-@@ -719,6 +782,7 @@ public class LevelChunk extends ChunkAccess {
- */
- org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
- server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
-+ this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system
-
- if (this.needsDecoration) {
- try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
-@@ -747,9 +811,11 @@ public class LevelChunk extends ChunkAccess {
- }
-
- public void unloadCallback() {
-+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
- org.bukkit.Server server = this.level.getCraftServer();
-+ this.chunkHolder.getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system
- org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
-- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
-+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below
- server.getPluginManager().callEvent(unloadEvent);
- // note: saving can be prevented, but not forced if no saving is actually required
- this.mustNotSave = !unloadEvent.isSaveChunk();
-@@ -771,9 +837,26 @@ public class LevelChunk extends ChunkAccess {
- // Paper end
- }
-
-+ // Paper start - add dirty system to tick lists
+ @Override
-+ public void setUnsaved(boolean needsSaving) {
-+ if (!needsSaving) {
-+ this.blockTicks.clearDirty();
-+ this.fluidTicks.clearDirty();
-+ }
-+ super.setUnsaved(needsSaving);
++ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
++
++ @Override
++ public boolean[] starlight$getSkyEmptinessMap() {
++ return null;
++ }
++
++ @Override
++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {}
++
++ @Override
++ public boolean[] starlight$getBlockEmptinessMap() {
++ return null;
+ }
-+ // Paper end - add dirty system to tick lists
++
++ @Override
++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {}
++ // Paper end - rewrite chunk system
+
@Override
- public boolean isUnsaved() {
-- return super.isUnsaved() && !this.mustNotSave;
-+ // Paper start - add dirty system to tick lists
-+ long gameTime = this.level.getLevelData().getGameTime();
-+ if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) {
-+ return true;
-+ }
-+ // Paper end - add dirty system to tick lists
-+ return super.isUnsaved(); // Paper - rewrite chunk system - do NOT clobber the dirty flag
+ public BlockState getBlockState(BlockPos pos) {
+ return Blocks.VOID_AIR.defaultBlockState();
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+index 365074be989aa4a178114fd5e9810f1a68640196..4af698930712389881601069a921f054c07935f2 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+@@ -31,7 +31,7 @@ import net.minecraft.world.level.material.FluidState;
+ import net.minecraft.world.ticks.BlackholeTickAccess;
+ import net.minecraft.world.ticks.TickContainerAccess;
+
+-public class ImposterProtoChunk extends ProtoChunk {
++public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
+ private final LevelChunk wrapped;
+ private final boolean allowWrites;
+
+@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk {
+ this.allowWrites = propagateToWrapped;
}
- // CraftBukkit end
-@@ -842,7 +925,9 @@ public class LevelChunk extends ChunkAccess {
- return this.blockEntities;
++ // Paper start - rewrite chunk system
++ @Override
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockNibbles();
++ }
++
++ @Override
++ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockNibbles(nibbles);
++ }
++
++ @Override
++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyNibbles();
++ }
++
++ @Override
++ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyNibbles(nibbles);
++ }
++
++ @Override
++ public boolean[] starlight$getSkyEmptinessMap() {
++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyEmptinessMap();
++ }
++
++ @Override
++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyEmptinessMap(emptinessMap);
++ }
++
++ @Override
++ public boolean[] starlight$getBlockEmptinessMap() {
++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockEmptinessMap();
++ }
++
++ @Override
++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockEmptinessMap(emptinessMap);
++ }
++ // Paper end - rewrite chunk system
++
+ @Nullable
+ @Override
+ public BlockEntity getBlockEntity(BlockPos pos) {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index 6c0e12c9c9c0fb8377cd1f48a43ca75c9fc3e58e..3be8f35ece18d4cffe8b23ecfeeff359e0b36e3e 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -53,7 +53,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
+ import net.minecraft.world.ticks.TickContainerAccess;
+ import org.slf4j.Logger;
+
+-public class LevelChunk extends ChunkAccess {
++public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
+
+ static final Logger LOGGER = LogUtils.getLogger();
+ private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
+@@ -218,6 +218,14 @@ public class LevelChunk extends ChunkAccess {
+ }
}
+ // Paper end
++ // Paper start - rewrite chunk system
++ private boolean postProcessingDone;
++
++ @Override
++ public final boolean moonrise$isPostProcessingDone() {
++ return this.postProcessingDone;
++ }
++ // Paper end - rewrite chunk system
-+ public boolean isPostProcessingDone; // Paper - replace chunk loader system
- public void postProcessGeneration() {
-+ try { // Paper - replace chunk loader system
- ChunkPos chunkcoordintpair = this.getPos();
+ public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
+ this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
+@@ -247,13 +255,19 @@ public class LevelChunk extends ChunkAccess {
+ }
+ }
- for (int i = 0; i < this.postProcessing.length; ++i) {
-@@ -863,6 +948,7 @@ public class LevelChunk extends ChunkAccess {
- BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition);
+- this.skyLightSources = protoChunk.skyLightSources;
++ // Paper - rewrite chunk system
+ this.setLightCorrect(protoChunk.isLightCorrect());
+ this.unsaved = true;
+ this.needsDecoration = true; // CraftBukkit
+ // CraftBukkit start
+ this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
+ // CraftBukkit end
++ // Paper start - rewrite chunk system
++ this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles());
++ this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles());
++ this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap());
++ this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap());
++ // Paper end - rewrite chunk system
+ }
- this.level.setBlock(blockposition, iblockdata1, 20);
-+ if (iblockdata1 != iblockdata) this.level.chunkSource.blockChanged(blockposition); // Paper - replace player chunk loader - notify since we send before processing full updates
- }
- }
+ @Override
+@@ -436,7 +450,7 @@ public class LevelChunk extends ChunkAccess {
+ ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-@@ -880,6 +966,10 @@ public class LevelChunk extends ChunkAccess {
+ gameprofilerfiller.push("updateSkyLightSources");
+- this.skyLightSources.update(this, j, i, l);
++ // Paper - rewrite chunk system
+ gameprofilerfiller.popPush("queueCheckLight");
+ this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
+ gameprofilerfiller.pop();
+@@ -777,8 +791,27 @@ public class LevelChunk extends ChunkAccess {
- this.pendingBlockEntities.clear();
- this.upgradeData.upgrade(this);
-+ } finally { // Paper start - replace chunk loader system
-+ this.isPostProcessingDone = true;
+ @Override
+ public boolean isUnsaved() {
+- return super.isUnsaved() && !this.mustNotSave;
++ // Paper start - rewrite chunk system
++ final long gameTime = this.level.getGameTime();
++ if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime)
++ || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) {
++ return true;
++ }
++
++ return super.isUnsaved();
++ // Paper end - rewrite chunk system
++ }
++
++ // Paper start - rewrite chunk system
++ @Override
++ public void setUnsaved(final boolean needsSaving) {
++ if (!needsSaving) {
++ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty();
++ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty();
+ }
-+ // Paper end - replace chunk loader system
++ super.setUnsaved(needsSaving);
}
++ // Paper end - rewrite chunk system
+ // CraftBukkit end
- @Nullable
-@@ -929,7 +1019,7 @@ public class LevelChunk extends ChunkAccess {
- }
+ public boolean isEmpty() {
+@@ -884,6 +917,7 @@ public class LevelChunk extends ChunkAccess {
- public FullChunkStatus getFullStatus() {
-- return this.fullStatus == null ? FullChunkStatus.FULL : (FullChunkStatus) this.fullStatus.get();
-+ return this.chunkHolder == null ? FullChunkStatus.INACCESSIBLE : this.chunkHolder.getChunkStatus(); // Paper - rewrite chunk system
+ this.pendingBlockEntities.clear();
+ this.upgradeData.upgrade(this);
++ this.postProcessingDone = true; // Paper - rewrite chunk system
}
- public void setFullStatus(Supplier<FullChunkStatus> levelTypeProvider) {
+ @Nullable
+diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+index 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -155,7 +155,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+ return this.get(this.strategy.getIndex(x, y, z));
+ }
+
+- protected T get(int index) {
++ public T get(int index) { // Paper - public
+ PalettedContainer.Data<T> data = this.data;
+ return data.palette.valueFor(data.storage.get(index));
+ }
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+index 7f302405a88766c2112539d24d3dd2e513f94985..207dc31afcf5ca5a59ab27ee263aa10f94a79559 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess {
+ }
+
+ if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) {
+- this.skyLightSources.update(this, m, j, o);
++ // Paper - rewrite chunk system
+ this.lightEngine.checkBlock(pos);
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
+ .step(ChunkStatus.CARVERS, builder -> builder)
+ .step(ChunkStatus.FEATURES, builder -> builder)
+ .step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight))
+- .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light))
++ .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours
+ .step(ChunkStatus.SPAWN, builder -> builder)
+ .step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full))
+ .build();
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-index 95318092f8281d98132d1d3ceb4a5c36cf32eb05..b81c548c0e1ac53784e9c94b34b65db5f123309c 100644
+index 0baa4adf2a4401f9c955352f27e6f99957d1dff4..3723c07183e7b894cccf4d01bedf1d0d832c1910 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-@@ -21,13 +21,15 @@ import net.minecraft.world.level.chunk.ProtoChunk;
+@@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.Heightmap;
+ import org.jetbrains.annotations.VisibleForTesting;
- public class ChunkStatus {
-+ static final ChunkStatus.LoadingTask PASSTHROUGH_LOAD_TASK = (WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) -> CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system
-+ protected static final java.util.List<ChunkStatus> statuses = new java.util.ArrayList<>(); // Paper - rewrite chunk system
+-public class ChunkStatus {
++public class ChunkStatus implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus { // Paper - rewrite chunk system
public static final int MAX_STRUCTURE_DISTANCE = 8;
- private static final EnumSet<Heightmap.Types> PRE_FEATURES = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
- public static final EnumSet<Heightmap.Types> POST_FEATURES = EnumSet.of(
- Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE, Heightmap.Types.MOTION_BLOCKING, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES
- );
- public static final ChunkStatus EMPTY = register(
-- "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, ChunkStatusTasks::loadPassThrough
-+ "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus STRUCTURE_STARTS = register(
- "structure_starts",
-@@ -47,22 +49,22 @@ public class ChunkStatus {
- PRE_FEATURES,
- ChunkType.PROTOCHUNK,
- ChunkStatusTasks::generateStructureReferences,
-- ChunkStatusTasks::loadPassThrough
-+ PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus BIOMES = register(
-- "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, ChunkStatusTasks::loadPassThrough
-+ "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus NOISE = register(
-- "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, ChunkStatusTasks::loadPassThrough
-+ "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus SURFACE = register(
-- "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, ChunkStatusTasks::loadPassThrough
-+ "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus CARVERS = register(
-- "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, ChunkStatusTasks::loadPassThrough
-+ "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus FEATURES = register(
-- "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, ChunkStatusTasks::loadPassThrough
-+ "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus INITIALIZE_LIGHT = register(
- "initialize_light",
-@@ -78,7 +80,7 @@ public class ChunkStatus {
- "light", INITIALIZE_LIGHT, 1, true, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateLight, ChunkStatusTasks::loadLight
- );
- public static final ChunkStatus SPAWN = register(
-- "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, ChunkStatusTasks::loadPassThrough
-+ "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system
- );
- public static final ChunkStatus FULL = register(
- "full", SPAWN, 0, false, POST_FEATURES, ChunkType.LEVELCHUNK, ChunkStatusTasks::generateFull, ChunkStatusTasks::loadFull
-@@ -128,6 +130,27 @@ public class ChunkStatus {
- }
+ private static final EnumSet<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
+ public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of(
+@@ -51,8 +51,68 @@ public class ChunkStatus {
+ return list;
}
- // Paper end - starlight
+
+ // Paper start - rewrite chunk system
-+ public boolean isParallelCapable; // Paper
-+ public int writeRadius = -1;
-+ public int loadRange = 0;
-+
++ private boolean isParallelCapable;
++ private boolean emptyLoadTask;
++ private int writeRadius;
+ private ChunkStatus nextStatus;
++ private java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete;
++
++ @Override
++ public final boolean moonrise$isParallelCapable() {
++ return this.isParallelCapable;
++ }
++
++ @Override
++ public final void moonrise$setParallelCapable(final boolean value) {
++ this.isParallelCapable = value;
++ }
++
++ @Override
++ public final int moonrise$getWriteRadius() {
++ return this.writeRadius;
++ }
++
++ @Override
++ public final void moonrise$setWriteRadius(final int value) {
++ this.writeRadius = value;
++ }
+
-+ public final ChunkStatus getNextStatus() {
++ @Override
++ public final ChunkStatus moonrise$getNextStatus() {
+ return this.nextStatus;
+ }
+
-+ public final boolean isEmptyLoadStatus() {
-+ return this.loadingTask == PASSTHROUGH_LOAD_TASK;
++ @Override
++ public final boolean moonrise$isEmptyLoadStatus() {
++ return this.emptyLoadTask;
++ }
++
++ @Override
++ public void moonrise$setEmptyLoadStatus(final boolean value) {
++ this.emptyLoadTask = value;
+ }
+
-+ public final boolean isEmptyGenStatus() {
-+ return this == ChunkStatus.EMPTY;
++ @Override
++ public final boolean moonrise$isEmptyGenStatus() {
++ return (Object)this == ChunkStatus.EMPTY;
+ }
+
-+ public final java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
++ @Override
++ public final java.util.concurrent.atomic.AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() {
++ return this.warnedAboutNoImmediateComplete;
++ }
+ // Paper end - rewrite chunk system
-
- private static ChunkStatus register(
- String id,
-@@ -190,6 +213,13 @@ public class ChunkStatus {
++
+ @VisibleForTesting
+ protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) {
++ this.isParallelCapable = false;
++ this.writeRadius = -1;
++ this.nextStatus = (ChunkStatus)(Object)this;
++ if (previous != null) {
++ previous.nextStatus = (ChunkStatus)(Object)this;
++ }
++ this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
+ this.parent = previous == null ? this : previous;
this.chunkType = chunkType;
this.heightmapsAfter = heightMapTypes;
- this.index = previous == null ? 0 : previous.getIndex() + 1;
-+ // Paper start
-+ this.nextStatus = this;
-+ if (statuses.size() > 0) {
-+ statuses.get(statuses.size() - 1).nextStatus = this;
-+ }
-+ statuses.add(this);
-+ // Paper end
- }
-
- public int getIndex() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-index ce7f154b9dad4e78ee0189405cf57dcb3d5301b8..a5e8078b99161272b0f826b8c39e56d17588c264 100644
+index ae16cf5c803caae636860dd9b1a83abe479ca5a4..b993c4b2595e2879b25753c2e34530f3622c18fa 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-@@ -26,8 +26,9 @@ public class ChunkStatusTasks {
- return CompletableFuture.completedFuture(chunk);
- }
-
-- static CompletableFuture<ChunkAccess> loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) {
-- return CompletableFuture.completedFuture(chunk);
-+ @io.papermc.paper.annotation.DoNotUse @Deprecated(forRemoval = true) // Paper - rewrite chunk system - use ChunkStatus.PASSTHROUGH_LOAD_TASK instead
-+ static CompletableFuture<ChunkAccess> loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) { // Paper - rewrite chunk system - diff on change
-+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system - diff on change
+@@ -154,7 +154,7 @@ public class ChunkStatusTasks {
+ chunk1 = ((ImposterProtoChunk) protochunk).getWrapped();
+ } else {
+ chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
+- ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
++ ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities(), protochunk.getPos()); // Paper - rewrite chunk system
+ });
+ generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
+ }
+@@ -175,7 +175,7 @@ public class ChunkStatusTasks {
+ });
}
- static CompletableFuture<ChunkAccess> generateStructureStarts(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List<ChunkAccess> chunks, ChunkAccess chunk) {
-@@ -125,7 +126,7 @@ public class ChunkStatusTasks {
- ((ProtoChunk) chunk).setLightEngine(lightingProvider);
- boolean flag = ChunkStatusTasks.isLighted(chunk);
+- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
++ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public, add ChunkPos param
+ if (!entities.isEmpty()) {
+ // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
+ world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world).filter((entity) -> {
+@@ -191,7 +191,7 @@ public class ChunkStatusTasks {
+ }
+ checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
+ return !needsRemoval;
+- }));
++ }), pos); // Paper - rewrite chunk system
+ // CraftBukkit end
+ }
-- return lightingProvider.initializeLight(chunk, flag);
-+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system
+diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
+index f6e08a8334633ff1532616d051bed46b702d0091..4e56398a6fb8b97199f4c74ebebc1055fb718dcf 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
+@@ -11,9 +11,50 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
+ import net.minecraft.world.level.chunk.ChunkAccess;
+ import net.minecraft.world.level.chunk.ProtoChunk;
+
+-public record ChunkStep(
+- ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
+-) {
++// Paper start - rewerite chunk system - convert record to class
++public final class ChunkStep implements ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep { // Paper - rewrite chunk system
++ private final ChunkStatus targetStatus;
++ private final ChunkDependencies directDependencies;
++ private final ChunkDependencies accumulatedDependencies;
++ private final int blockStateWriteRadius;
++ private final ChunkStatusTask task;
++
++ private final ChunkStatus[] byRadius; // Paper - rewrite chunk system
++
++ public ChunkStep(
++ ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
++ ) {
++ this.targetStatus = targetStatus;
++ this.directDependencies = directDependencies;
++ this.accumulatedDependencies = accumulatedDependencies;
++ this.blockStateWriteRadius = blockStateWriteRadius;
++ this.task = task;
++
++ // Paper start - rewrite chunk system
++ this.byRadius = new ChunkStatus[this.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + 1];
++ this.byRadius[0] = targetStatus.getParent();
++
++ for (ChunkStatus status = targetStatus.getParent(); status != ChunkStatus.EMPTY; status = status.getParent()) {
++ final int radius = this.getAccumulatedRadiusOf(status);
++
++ for (int j = 0; j <= radius; ++j) {
++ if (this.byRadius[j] == null) {
++ this.byRadius[j] = status;
++ }
++ }
++ }
++ // Paper end - rewrite chunk system
++ }
++
++ // Paper start - rewrite chunk system
++ @Override
++ public final ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius) {
++ return this.byRadius[radius];
++ }
++ // Paper end - rewrite chunk system
++
++ // Paper start - rewerite chunk system - convert record to class
++
+ public int getAccumulatedRadiusOf(ChunkStatus status) {
+ return status == this.targetStatus ? 0 : this.accumulatedDependencies.getRadiusOf(status);
}
-
- static CompletableFuture<ChunkAccess> generateLight(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List<ChunkAccess> chunks, ChunkAccess chunk) {
-@@ -139,7 +140,7 @@ public class ChunkStatusTasks {
- private static CompletableFuture<ChunkAccess> lightChunk(ThreadedLevelLightEngine lightingProvider, ChunkAccess chunk) {
- boolean flag = ChunkStatusTasks.isLighted(chunk);
-
-- return lightingProvider.lightChunk(chunk, flag);
-+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system
+@@ -39,6 +80,56 @@ public record ChunkStep(
+ return chunk;
}
- static CompletableFuture<ChunkAccess> generateSpawn(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List<ChunkAccess> chunks, ChunkAccess chunk) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..5f85d8d82212f9a8133304dc05bf2cd39da1f9e7 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-@@ -112,7 +112,25 @@ public class ChunkSerializer {
- }
- }
- // Paper end - guard against serializing mismatching coordinates
-+ // Paper start - rewrite chunk system
-+ public static final class InProgressChunkHolder {
++ // Paper start - rewerite chunk system - convert record to class
++ public ChunkStatus targetStatus() {
++ return targetStatus;
++ }
+
-+ public final ProtoChunk protoChunk;
++ public ChunkDependencies directDependencies() {
++ return directDependencies;
++ }
+
-+ public CompoundTag poiData;
++ public ChunkDependencies accumulatedDependencies() {
++ return accumulatedDependencies;
++ }
+
-+ public InProgressChunkHolder(final ProtoChunk protoChunk) {
-+ this.protoChunk = protoChunk;
-+ }
++ public int blockStateWriteRadius() {
++ return blockStateWriteRadius;
+ }
- public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) {
-+ // Paper start - rewrite chunk system
-+ InProgressChunkHolder holder = readInProgressChunkHolder(world, poiStorage, chunkPos, nbt);
-+ return holder.protoChunk;
++
++ public ChunkStatusTask task() {
++ return task;
+ }
+
-+ public static InProgressChunkHolder readInProgressChunkHolder(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) {
-+ // Paper end - rewrite chunk system
- // Paper start - Do not let the server load chunks from newer versions
- if (nbt.contains("DataVersion", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
- final int dataVersion = nbt.getInt("DataVersion");
-@@ -178,7 +196,7 @@ public class ChunkSerializer {
++ @Override
++ public boolean equals(Object obj) {
++ if (obj == this) return true;
++ if (obj == null || obj.getClass() != this.getClass()) return false;
++ var that = (net.minecraft.world.level.chunk.status.ChunkStep) obj;
++ return java.util.Objects.equals(this.targetStatus, that.targetStatus) &&
++ java.util.Objects.equals(this.directDependencies, that.directDependencies) &&
++ java.util.Objects.equals(this.accumulatedDependencies, that.accumulatedDependencies) &&
++ this.blockStateWriteRadius == that.blockStateWriteRadius &&
++ java.util.Objects.equals(this.task, that.task);
++ }
++
++ @Override
++ public int hashCode() {
++ return java.util.Objects.hash(targetStatus, directDependencies, accumulatedDependencies, blockStateWriteRadius, task);
++ }
++
++ @Override
++ public String toString() {
++ return "ChunkStep[" +
++ "targetStatus=" + targetStatus + ", " +
++ "directDependencies=" + directDependencies + ", " +
++ "accumulatedDependencies=" + accumulatedDependencies + ", " +
++ "blockStateWriteRadius=" + blockStateWriteRadius + ", " +
++ "task=" + task + ']';
++ }
++ // Paper end - rewerite chunk system - convert record to class
++
++
+ public static class Builder {
+ private final ChunkStatus status;
+ @Nullable
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+index d42585bccb03f8ee1be5e37cfbe8520af4cc5454..977bebe8657abc5cb84ede8276d6781cde20e847 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+@@ -165,7 +165,7 @@ public class ChunkSerializer {
achunksection[k] = chunksection;
SectionPos sectionposition = SectionPos.of(chunkPos, b0);
@@ -20418,35 +27330,23 @@ index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..5f85d8d82212f9a8133304dc05bf2cd3
}
boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
-@@ -325,7 +343,7 @@ public class ChunkSerializer {
+@@ -287,6 +287,8 @@ public class ChunkSerializer {
+ }
}
++ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.loadLightHook(world, chunkPos, nbt, (ChunkAccess)object1); // Paper - rewrite chunk system - note: it's ok to pass the raw value instead of wrapped
++
if (chunktype == ChunkType.LEVELCHUNK) {
-- return new ImposterProtoChunk((LevelChunk) object1, false);
-+ return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false)); // Paper - Async chunk loading
+ return new ImposterProtoChunk((LevelChunk) object1, false);
} else {
- ProtoChunk protochunk1 = (ProtoChunk) object1;
-
-@@ -360,9 +378,41 @@ public class ChunkSerializer {
- protochunk1.setCarvingMask(worldgenstage_features, new CarvingMask(nbttagcompound5.getLongArray(s1), ((ChunkAccess) object1).getMinBuildHeight()));
- }
+@@ -341,14 +343,44 @@ public class ChunkSerializer {
+ }
+ // CraftBukkit end
-- return protochunk1;
-+ return new InProgressChunkHolder(protochunk1); // Paper - Async chunk loading
-+ }
-+ }
-+
-+ // Paper start - async chunk save for unload
-+ public record AsyncSaveData(
-+ Tag blockTickList, // non-null if we had to go to the server's tick list
-+ Tag fluidTickList, // non-null if we had to go to the server's tick list
-+ ListTag blockEntities,
-+ long worldTime
-+ ) {}
-+
++ // Paper start - async chunk saving
+ // must be called sync
-+ public static AsyncSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
-+ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save");
++ public static ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
++ io.papermc.paper.util.TickThread.ensureTickThread(world, chunk.locX, chunk.locZ, "Preparing async chunk save data");
+
+ final CompoundTag tickLists = new CompoundTag();
+ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization());
@@ -20457,68 +27357,62 @@ index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..5f85d8d82212f9a8133304dc05bf2cd3
+ if (blockEntityNbt != null) {
+ blockEntitiesSerialized.add(blockEntityNbt);
+ }
- }
++ }
+
-+ return new AsyncSaveData(
++ return new ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData(
+ tickLists.get(BLOCK_TICKS_TAG),
+ tickLists.get(FLUID_TICKS_TAG),
+ blockEntitiesSerialized,
+ world.getGameTime()
+ );
- }
-+ // Paper end
-
- private static void logErrors(ChunkPos chunkPos, int y, String message) {
- ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message);
-@@ -379,6 +429,11 @@ public class ChunkSerializer {
- // CraftBukkit end
-
++ }
++ // Paper end - async chunk saving
++
public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
-+ // Paper start
++ // Paper start - async chunk saving
+ return saveChunk(world, chunk, null);
+ }
-+ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, @org.checkerframework.checker.nullness.qual.Nullable AsyncSaveData asyncsavedata) {
-+ // Paper end
- // Paper start - rewrite light impl
- final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world);
- final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world);
-@@ -391,7 +446,7 @@ public class ChunkSerializer {
++ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData asyncsavedata) {
++ // Paper end - async chunk saving
+ ChunkPos chunkcoordintpair = chunk.getPos();
+ CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag());
+
nbttagcompound.putInt("xPos", chunkcoordintpair.x);
nbttagcompound.putInt("yPos", chunk.getMinSection());
nbttagcompound.putInt("zPos", chunkcoordintpair.z);
- nbttagcompound.putLong("LastUpdate", world.getGameTime());
-+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading
++ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime() : world.getGameTime()); // Paper - async chunk saving
nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime());
- nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString());
+ nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
-@@ -485,8 +540,17 @@ public class ChunkSerializer {
- nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_)
+@@ -424,8 +456,17 @@ public class ChunkSerializer {
+ nbttagcompound.putBoolean("isLightOn", true);
}
- ListTag nbttaglist1 = new ListTag();
- Iterator iterator = chunk.getBlockEntitiesPos().iterator();
-+ // Paper start
++ // Paper start - async chunk saving
+ ListTag nbttaglist1;
+ Iterator<BlockPos> iterator;
+ if (asyncsavedata != null) {
-+ nbttaglist1 = asyncsavedata.blockEntities;
++ nbttaglist1 = asyncsavedata.blockEntities();
+ iterator = java.util.Collections.emptyIterator();
+ } else {
+ nbttaglist1 = new ListTag();
+ iterator = chunk.getBlockEntitiesPos().iterator();
+ }
-+ // Paper end
++ // Paper end - async chunk saving
CompoundTag nbttagcompound2;
-@@ -522,7 +586,14 @@ public class ChunkSerializer {
+@@ -461,7 +502,14 @@ public class ChunkSerializer {
nbttagcompound.put("CarvingMasks", nbttagcompound2);
}
+ // Paper start
+ if (asyncsavedata != null) {
-+ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList);
-+ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList);
++ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList());
++ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList());
+ } else {
ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization());
+ }
@@ -20526,7 +27420,15 @@ index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..5f85d8d82212f9a8133304dc05bf2cd3
nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing()));
CompoundTag nbttagcompound3 = new CompoundTag();
Iterator iterator1 = chunk.getHeightmaps().iterator();
-@@ -578,7 +649,7 @@ public class ChunkSerializer {
+@@ -481,6 +529,7 @@ public class ChunkSerializer {
+ nbttagcompound.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
+ }
+ // CraftBukkit end
++ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.saveLightHook(world, chunk, nbttagcompound); // Paper - rewrite chunk system
+ return nbttagcompound;
+ }
+
+@@ -506,7 +555,7 @@ public class ChunkSerializer {
return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> {
if (nbttaglist != null) {
@@ -20536,26 +27438,37 @@ index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..5f85d8d82212f9a8133304dc05bf2cd3
if (nbttaglist1 != null) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index a62c90e10c0dfa4c6211a05c4071932756d7b218..554dede2ad0e45d3ee4ccc5510b7644f2e9e4250 100644
+index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..0cdc224656a2baa09b7dfbb249b6a96320ac43e0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -31,18 +31,21 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
- public class ChunkStorage implements AutoCloseable {
+@@ -28,21 +28,31 @@ import net.minecraft.world.level.dimension.LevelStem;
+ import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
+ import net.minecraft.world.level.storage.DimensionDataStorage;
+
+-public class ChunkStorage implements AutoCloseable {
++public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage { // Paper - rewrite chunk system
public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
- private final IOWorker worker;
-+ // Paper start - rewrite chunk system; async chunk IO
-+ private final Object persistentDataLock = new Object();
-+ public final RegionFileStorage regionFileCache;
-+ // Paper end - rewrite chunk system
++ // Paper - rewrite chunk system
protected final DataFixer fixerUpper;
@Nullable
private volatile LegacyStructureDataHandler legacyStructureHandler;
++ // Paper start - rewrite chunk system
++ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger();
++ private final RegionFileStorage storage;
++
++ @Override
++ public final RegionFileStorage moonrise$getRegionStorage() {
++ return this.storage;
++ }
++ // Paper end - rewrite chunk system
++
public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) {
this.fixerUpper = dataFixer;
- this.worker = new IOWorker(storageKey, directory, dsync);
-+ this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync); // Paper - rewrite chunk system; async chunk IO
++ this.storage = new IOWorker(storageKey, directory, dsync).storage; // Paper - rewrite chunk system
}
public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
@@ -20564,352 +27477,223 @@ index a62c90e10c0dfa4c6211a05c4071932756d7b218..554dede2ad0e45d3ee4ccc5510b7644f
}
// CraftBukkit start
-@@ -50,8 +53,9 @@ public class ChunkStorage implements AutoCloseable {
- if (true) return true; // Paper - Perf: this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full"
- ChunkPos pos = new ChunkPos(x, z);
- if (cps != null) {
-- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
-- if (cps.hasChunk(x, z)) {
-+ // Paper start - rewrite chunk system; async chunk IO
-+ if (cps.getChunkAtIfCachedImmediately(x, z) != null) { // isLoaded is a ticket level check, not a chunk loaded check!
-+ // Paper end - rewrite chunk system
- return true;
- }
- }
-@@ -79,6 +83,7 @@ public class ChunkStorage implements AutoCloseable {
-
- public CompoundTag upgradeChunkTag(ResourceKey<LevelStem> resourcekey, Supplier<DimensionDataStorage> supplier, CompoundTag nbttagcompound, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> optional, ChunkPos pos, @Nullable LevelAccessor generatoraccess) {
- // CraftBukkit end
-+ nbttagcompound = nbttagcompound.copy(); // Paper - defensive copy, another thread might modify this
- int i = ChunkStorage.getVersion(nbttagcompound);
+@@ -102,7 +112,9 @@ public class ChunkStorage implements AutoCloseable {
+ if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
+ LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
- try {
-@@ -97,9 +102,11 @@ public class ChunkStorage implements AutoCloseable {
- if (i < 1493) {
- ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter
- if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
-+ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
- LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
-
- nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
-+ } // Paper - Async chunk loading
++ synchronized (persistentstructurelegacy) { // Paper - rewrite chunk system
+ nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
++ } // Paper - rewrite chunk system
+ }
}
- }
-@@ -139,7 +146,7 @@ public class ChunkStorage implements AutoCloseable {
- LegacyStructureDataHandler persistentstructurelegacy = this.legacyStructureHandler;
-
- if (persistentstructurelegacy == null) {
-- synchronized (this) {
-+ synchronized (this.persistentDataLock) { // Paper - async chunk loading
- persistentstructurelegacy = this.legacyStructureHandler;
- if (persistentstructurelegacy == null) {
- this.legacyStructureHandler = persistentstructurelegacy = LegacyStructureDataHandler.getLegacyStructureHandler(worldKey, (DimensionDataStorage) stateManagerGetter.get());
-@@ -165,10 +172,20 @@ public class ChunkStorage implements AutoCloseable {
+@@ -169,7 +181,13 @@ public class ChunkStorage implements AutoCloseable {
}
public CompletableFuture<Optional<CompoundTag>> read(ChunkPos chunkPos) {
- return this.worker.loadAsync(chunkPos);
-+ // Paper start - async chunk io
++ // Paper start - rewrite chunk system
+ try {
-+ return CompletableFuture.completedFuture(Optional.ofNullable(this.readSync(chunkPos)));
-+ } catch (Throwable thr) {
-+ return CompletableFuture.failedFuture(thr);
++ return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos)));
++ } catch (final Throwable throwable) {
++ return CompletableFuture.failedFuture(throwable);
+ }
-+ }
-+ @Nullable
-+ public CompoundTag readSync(ChunkPos chunkPos) throws IOException {
-+ return this.regionFileCache.read(chunkPos);
++ // Paper end - rewrite chunk system
}
-+ // Paper end - async chunk io
-- public CompletableFuture<Void> write(ChunkPos chunkPos, CompoundTag nbt) {
-+ public CompletableFuture<Void> write(ChunkPos chunkPos, CompoundTag nbt) throws IOException { // Paper - rewrite chunk system; async chunk io
- // Paper start - guard against serializing mismatching coordinates
- if (nbt != null && !chunkPos.equals(ChunkSerializer.getChunkCoordinate(nbt))) {
- final String world = (this instanceof net.minecraft.server.level.ChunkMap) ? ((net.minecraft.server.level.ChunkMap) this).level.getWorld().getName() : null;
-@@ -176,26 +193,39 @@ public class ChunkStorage implements AutoCloseable {
- + " but compound says coordinate is " + ChunkSerializer.getChunkCoordinate(nbt) + (world == null ? " for an unknown world" : (" for world: " + world)));
+ public CompletableFuture<Void> write(ChunkPos chunkPos, CompoundTag nbt) {
+@@ -181,29 +199,54 @@ public class ChunkStorage implements AutoCloseable {
}
// Paper end - guard against serializing mismatching coordinates
-+ this.regionFileCache.write(chunkPos, nbt); // Paper - rewrite chunk system; async chunk io, move above legacy structure index
this.handleLegacyStructureIndex(chunkPos);
- return this.worker.store(chunkPos, nbt);
-+ // Paper - rewrite chunk system; async chunk io, move above legacy structure index
-+ return null;
++ // Paper start - rewrite chunk system
++ try {
++ this.storage.write(chunkPos, nbt);
++ return CompletableFuture.completedFuture(null);
++ } catch (final Throwable throwable) {
++ return CompletableFuture.failedFuture(throwable);
++ }
++ // Paper end - rewrite chunk system
}
protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
if (this.legacyStructureHandler != null) {
-+ synchronized (this.persistentDataLock) { // Paper - rewrite chunk system; async chunk io
++ synchronized (this.legacyStructureHandler) { // Paper - rewrite chunk system
this.legacyStructureHandler.removeIndex(chunkPos.toLong());
-+ } // Paper - rewrite chunk system; async chunk io
++ } // Paper - rewrite chunk system
}
}
public void flushWorker() {
- this.worker.synchronize(true).join();
-+ io.papermc.paper.chunk.system.io.RegionFileIOThread.flush(); // Paper - rewrite chunk system
++ // Paper start - rewrite chunk system
++ try {
++ this.storage.flush();
++ } catch (final IOException ex) {
++ LOGGER.error("Failed to flush chunk storage", ex);
++ }
++ // Paper end - rewrite chunk system
}
public void close() throws IOException {
- this.worker.close();
-+ this.regionFileCache.close(); // Paper - nuke IO worker
++ this.storage.close(); // Paper - rewrite chunk system
}
public ChunkScanAccess chunkScanner() {
- return this.worker;
-+ // Paper start - nuke IO worker
-+ return ((chunkPos, streamTagVisitor) -> {
++ // Paper start - rewrite chunk system
++ // TODO ChunkMap implementation?
++ return (chunkPos, streamTagVisitor) -> {
+ try {
-+ this.regionFileCache.scanChunk(chunkPos, streamTagVisitor);
++ this.storage.scanChunk(chunkPos, streamTagVisitor);
+ return java.util.concurrent.CompletableFuture.completedFuture(null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
-+ });
-+ // Paper end
++ };
++ // Paper end - rewrite chunk system
+ }
+
+- protected RegionStorageInfo storageInfo() {
+- return this.worker.storageInfo();
++ public RegionStorageInfo storageInfo() { // Paper - public
++ return this.storage.info(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
-index 49d8a62d2b6ca6da4e02b3cec7e42c38b7781b57..9fdf8f857a5f9b231c6d0633eaba498244214f74 100644
+index 36b8a9ac385e43f3212aca1b1f5bd7115bd00431..503ac0374e0c9f9993ad37bb8bd8cf1570d3615a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
-@@ -27,43 +27,30 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
- private static final String ENTITIES_TAG = "Entities";
- private static final String POSITION_TAG = "Position";
- public final ServerLevel level;
-- private final SimpleRegionStorage simpleRegionStorage;
-+ // Paper - rewrite chunk system
- private final LongSet emptyChunks = new LongOpenHashSet();
-- public final ProcessorMailbox<Runnable> entityDeserializerQueue;
-+ // Paper - rewrite chunk system
-
- public EntityStorage(SimpleRegionStorage storage, ServerLevel world, Executor executor) {
-- this.simpleRegionStorage = storage;
-+ // Paper - rewrite chunk system
- this.level = world;
-- this.entityDeserializerQueue = ProcessorMailbox.create(executor, "entity-deserializer");
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public CompletableFuture<ChunkEntities<Entity>> loadEntities(ChunkPos pos) {
-- return this.emptyChunks.contains(pos.toLong())
-- ? CompletableFuture.completedFuture(emptyChunk(pos))
-- : this.simpleRegionStorage.read(pos).thenApplyAsync(nbt -> {
-- if (nbt.isEmpty()) {
-- this.emptyChunks.add(pos.toLong());
-- return emptyChunk(pos);
-- } else {
-- try {
-- ChunkPos chunkPos2 = readChunkPos(nbt.get());
-- if (!Objects.equals(pos, chunkPos2)) {
-- LOGGER.error("Chunk file at {} is in the wrong location. (Expected {}, got {})", pos, pos, chunkPos2);
-- }
-- } catch (Exception var6) {
-- LOGGER.warn("Failed to parse chunk {} position info", pos, var6);
-- }
--
-- CompoundTag compoundTag = this.simpleRegionStorage.upgradeChunkTag(nbt.get(), -1);
-- ListTag listTag = compoundTag.getList("Entities", 10);
-- List<Entity> list = EntityType.loadEntitiesRecursive(listTag, this.level).collect(ImmutableList.toImmutableList());
-- return new ChunkEntities<>(pos, list);
-- }
-- }, this.entityDeserializerQueue::tell);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - copy out read logic into readEntities
+@@ -70,12 +70,12 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
+ }
}
- private static ChunkPos readChunkPos(CompoundTag chunkNbt) {
-+ // Paper start - rewrite chunk system
-+ public static List<Entity> readEntities(ServerLevel level, CompoundTag compoundTag) {
-+ ListTag listTag = compoundTag.getList("Entities", 10);
-+ List<Entity> list = EntityType.loadEntitiesRecursive(listTag, level).collect(ImmutableList.toImmutableList());
-+ return list;
-+ }
-+ // Paper end - rewrite chunk system
-+
+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public
int[] is = chunkNbt.getIntArray("Position");
return new ChunkPos(is[0], is[1]);
}
-@@ -78,38 +65,74 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
- @Override
- public void storeEntities(ChunkEntities<Entity> dataList) {
-+ // Paper start - rewrite chunk system
-+ if (true) {
-+ throw new UnsupportedOperationException();
-+ }
-+ // Paper end - rewrite chunk system
- ChunkPos chunkPos = dataList.getPos();
- if (dataList.isEmpty()) {
- if (this.emptyChunks.add(chunkPos.toLong())) {
-- this.simpleRegionStorage.write(chunkPos, null);
-+ // Paper - rewrite chunk system - fix compile for unused field in dead code
- }
- } else {
-- ListTag listTag = new ListTag();
-- dataList.getEntities().forEach(entity -> {
-- CompoundTag compoundTagx = new CompoundTag();
-- if (entity.save(compoundTagx)) {
-- listTag.add(compoundTagx);
-- }
-- });
-- CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag());
-- compoundTag.put("Entities", listTag);
-- writeChunkPos(compoundTag, chunkPos);
-- this.simpleRegionStorage.write(chunkPos, compoundTag).exceptionally(ex -> {
-- LOGGER.error("Failed to store chunk {}", chunkPos, ex);
-- return null;
-- });
-+ // Paper - move into saveEntityChunk0
- this.emptyChunks.remove(chunkPos.toLong());
- }
+- private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) {
++ public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public
+ chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
}
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
+index 053504cc6c98be3b70bd1722e279d861694e015d..316bf111fe94ce7a71af71cd32c94fcf528d4365 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
+@@ -32,7 +32,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final AtomicBoolean shutdownRequested = new AtomicBoolean();
+ private final ProcessorMailbox<StrictQueue.IntRunnable> mailbox;
+- private final RegionFileStorage storage;
++ public final RegionFileStorage storage; // Paper - public
+ private final Map<ChunkPos, IOWorker.PendingStore> pendingWrites = Maps.newLinkedHashMap();
+ private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>();
+ private static final int REGION_CACHE_SIZE = 1024;
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e77fe5c37c 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -17,7 +17,7 @@ import net.minecraft.nbt.StreamTagVisitor;
+ import net.minecraft.util.ExceptionCollector;
+ import net.minecraft.world.level.ChunkPos;
+
+-public final class RegionFileStorage implements AutoCloseable {
++public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
+
+ public static final String ANVIL_EXTENSION = ".mca";
+ private static final int MAX_CACHE_SIZE = 256;
+@@ -26,33 +26,122 @@ public final class RegionFileStorage implements AutoCloseable {
+ private final Path folder;
+ private final boolean sync;
+
+- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
+ // Paper start - rewrite chunk system
-+ public static void copyEntities(final CompoundTag from, final CompoundTag into) {
-+ if (from == null) {
-+ return;
-+ }
-+ final ListTag entitiesFrom = from.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND);
-+ if (entitiesFrom == null || entitiesFrom.isEmpty()) {
-+ return;
-+ }
-+
-+ final ListTag entitiesInto = into.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND);
-+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities
-+ entitiesInto.addAll(0, entitiesFrom.copy()); // need to copy, this is coming from the save thread
++ private static final int REGION_SHIFT = 5;
++ private static final int MAX_NON_EXISTING_CACHE = 1024 * 64;
++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1);
++ private static String getRegionFileName(final int chunkX, final int chunkZ) {
++ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca";
+ }
+
-+ public static CompoundTag saveEntityChunk(List<Entity> entities, ChunkPos chunkPos, ServerLevel level) {
-+ return saveEntityChunk0(entities, chunkPos, level, false);
++ private boolean doesRegionFilePossiblyExist(final long position) {
++ synchronized (this.nonExistingRegionFiles) {
++ if (this.nonExistingRegionFiles.contains(position)) {
++ this.nonExistingRegionFiles.addAndMoveToFirst(position);
++ return false;
++ }
++ return true;
++ }
+ }
-+ private static CompoundTag saveEntityChunk0(List<Entity> entities, ChunkPos chunkPos, ServerLevel level, boolean force) {
-+ if (!force && entities.isEmpty()) {
-+ return null;
++
++ private void createRegionFile(final long position) {
++ synchronized (this.nonExistingRegionFiles) {
++ this.nonExistingRegionFiles.remove(position);
+ }
++ }
+
-+ ListTag listTag = new ListTag();
-+ entities.forEach((entity) -> { // diff here: use entities parameter
-+ CompoundTag compoundTag = new CompoundTag();
-+ if (entity.save(compoundTag)) {
-+ listTag.add(compoundTag);
++ private void markNonExisting(final long position) {
++ synchronized (this.nonExistingRegionFiles) {
++ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) {
++ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) {
++ this.nonExistingRegionFiles.removeLastLong();
++ }
+ }
++ }
++ }
+
-+ });
-+ CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag());
-+ compoundTag.put("Entities", listTag);
-+ writeChunkPos(compoundTag, chunkPos);
-+ // Paper - remove worker usage
-+
-+ return !force && listTag.isEmpty() ? null : compoundTag;
++ @Override
++ public final boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ) {
++ return !this.doesRegionFilePossiblyExist(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
+
-+ public static CompoundTag upgradeChunkTag(CompoundTag chunkNbt) {
-+ int i = NbtUtils.getDataVersion(chunkNbt, -1);
-+ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, chunkNbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion());
++ @Override
++ public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) {
++ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
-+ // Paper end - rewrite chunk system
+
- @Override
- public void flush(boolean sync) {
-- this.simpleRegionStorage.synchronize(sync).join();
-- this.entityDeserializerQueue.runAll();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public void close() throws IOException {
-- this.simpleRegionStorage.close();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-index e858436bcf1b234d4bc6e6a117f5224d5c2d9f90..307196b2a58d4f8db3e6e3c3517a8004d4908b13 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-@@ -48,6 +48,7 @@ public class RegionFile implements AutoCloseable {
- private final IntBuffer timestamps;
- @VisibleForTesting
- protected final RegionBitmap usedSectors;
-+ public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper
-
- public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException {
- this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format
-@@ -250,7 +251,7 @@ public class RegionFile implements AutoCloseable {
- return (byteCount + 4096 - 1) / 4096;
- }
-
-- public boolean doesChunkExist(ChunkPos pos) {
-+ public synchronized boolean doesChunkExist(ChunkPos pos) { // Paper - synchronized
- int i = this.getOffset(pos);
-
- if (i == 0) {
-@@ -417,6 +418,11 @@ public class RegionFile implements AutoCloseable {
- }
-
- public void close() throws IOException {
-+ // Paper start - Prevent regionfiles from being closed during use
-+ this.fileLock.lock();
-+ synchronized (this) {
-+ try {
-+ // Paper end
- try {
- this.padToFullSector();
- } finally {
-@@ -426,6 +432,10 @@ public class RegionFile implements AutoCloseable {
- this.file.close();
- }
- }
-+ } finally { // Paper start - Prevent regionfiles from being closed during use
-+ this.fileLock.unlock();
++ @Override
++ public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException {
++ final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
++
++ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
++ if (ret != null) {
++ return ret;
+ }
-+ } // Paper end
-
- }
-
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index c4eef3aade889c69cefd873bec2d031cc54103ea..3f6955be976064eb542b5c50a9d6d74457c1833c 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -26,31 +26,99 @@ public class RegionFileStorage implements AutoCloseable {
- private final Path folder;
- private final boolean sync;
-
-- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
-+ // Paper start - cache regionfile does not exist state
-+ static final int MAX_NON_EXISTING_CACHE = 1024 * 64;
-+ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet();
-+ private synchronized boolean doesRegionFilePossiblyExist(long position) {
-+ if (this.nonExistingRegionFiles.contains(position)) {
-+ this.nonExistingRegionFiles.addAndMoveToFirst(position);
-+ return false;
++
++ if (!this.doesRegionFilePossiblyExist(key)) {
++ return null;
+ }
-+ return true;
-+ }
+
-+ private synchronized void createRegionFile(long position) {
-+ this.nonExistingRegionFiles.remove(position);
-+ }
++ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
++ this.regionCache.removeLast().close();
++ }
+
-+ private synchronized void markNonExisting(long position) {
-+ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) {
-+ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) {
-+ this.nonExistingRegionFiles.removeLastLong();
-+ }
++ final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ));
++
++ if (!java.nio.file.Files.exists(regionPath)) {
++ this.markNonExisting(key);
++ return null;
+ }
-+ }
+
-+ public synchronized boolean doesRegionFileNotExistNoIO(ChunkPos pos) {
-+ long key = ChunkPos.asLong(pos.getRegionX(), pos.getRegionZ());
-+ return !this.doesRegionFilePossiblyExist(key);
++ this.createRegionFile(key);
++
++ FileUtil.createDirectoriesSafe(this.folder);
++
++ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
++
++ this.regionCache.putAndMoveToFirst(key, ret);
++
++ return ret;
+ }
-+ // Paper end - cache regionfile does not exist state
++ // Paper end - rewrite chunk system
+
-+ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected constructor
++ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
this.folder = directory;
this.sync = dsync;
this.info = storageKey;
@@ -20917,194 +27701,180 @@ index c4eef3aade889c69cefd873bec2d031cc54103ea..3f6955be976064eb542b5c50a9d6d744
- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
-+ // Paper start
-+ public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) {
-+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()));
-+ }
-+
-+ public synchronized boolean chunkExists(ChunkPos pos) throws IOException {
-+ RegionFile regionfile = getRegionFile(pos, true);
-+
-+ return regionfile != null ? regionfile.hasChunk(pos) : false;
-+ }
-+
-+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
-+ return this.getRegionFile(chunkcoordintpair, existingOnly, false);
-+ }
-+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException {
-+ // Paper end
-+ long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); final long regionPos = i; // Paper - OBFHELPER
- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
+- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
++ public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
++ // Paper start - rewrite chunk system
++ if (existingOnly) {
++ return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z);
++ }
++ synchronized (this) {
++ final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
- if (regionfile != null) {
-+ // Paper start
-+ if (lock) {
-+ // must be in this synchronized block
-+ regionfile.fileLock.lock();
-+ }
-+ // Paper end
- return regionfile;
- } else {
-+ // Paper start - cache regionfile does not exist state
-+ if (existingOnly && !this.doesRegionFilePossiblyExist(regionPos)) {
-+ return null;
+- if (regionfile != null) {
+- return regionfile;
+- } else {
+- if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
+- ((RegionFile) this.regionCache.removeLast()).close();
++ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
++ if (ret != null) {
++ return ret;
+ }
-+ // Paper end - cache regionfile does not exist state
- if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
- ((RegionFile) this.regionCache.removeLast()).close();
++
++ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
++ this.regionCache.removeLast().close();
}
-- FileUtil.createDirectoriesSafe(this.folder);
-+ // Paper - only create directory if not existing only - moved down
- Path path = this.folder;
- int j = chunkcoordintpair.getRegionX();
- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
++ final Path regionPath = this.folder.resolve(getRegionFileName(chunkcoordintpair.x, chunkcoordintpair.z));
++
++ this.createRegionFile(key);
++
+ FileUtil.createDirectoriesSafe(this.folder);
+- Path path = this.folder;
+- int j = chunkcoordintpair.getRegionX();
+- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
-+ if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state
-+ this.markNonExisting(regionPos);
-+ return null; // CraftBukkit
-+ } else {
-+ this.createRegionFile(regionPos);
-+ }
-+ // Paper end - cache regionfile does not exist state
-+ FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above
- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
+- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
- this.regionCache.putAndMoveToFirst(i, regionfile1);
-+ // Paper start
-+ if (lock) {
-+ // must be in this synchronized block
-+ regionfile1.fileLock.lock();
-+ }
-+ // Paper end
- return regionfile1;
+- this.regionCache.putAndMoveToFirst(i, regionfile1);
+- return regionfile1;
++ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
++
++ this.regionCache.putAndMoveToFirst(key, ret);
++
++ return ret;
}
++ // Paper end - rewrite chunk system
}
-@@ -58,11 +126,12 @@ public class RegionFileStorage implements AutoCloseable {
- @Nullable
- public CompoundTag read(ChunkPos pos) throws IOException {
- // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
-- RegionFile regionfile = this.getRegionFile(pos, true);
-+ RegionFile regionfile = this.getRegionFile(pos, true, true); // Paper
- if (regionfile == null) {
- return null;
- }
- // CraftBukkit end
-+ try { // Paper
- DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
-
- CompoundTag nbttagcompound;
-@@ -99,6 +168,9 @@ public class RegionFileStorage implements AutoCloseable {
- }
- return nbttagcompound;
-+ } finally { // Paper start
-+ regionfile.fileLock.unlock();
-+ } // Paper end
- }
+ @Nullable
+@@ -132,8 +221,14 @@ public final class RegionFileStorage implements AutoCloseable {
- public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException {
-@@ -133,7 +205,13 @@ public class RegionFileStorage implements AutoCloseable {
}
- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
+- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
++ public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - public
++ RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system
+ // Paper start - rewrite chunk system
-+ RegionFile regionfile = this.getRegionFile(pos, nbt == null, true); // CraftBukkit
-+ if (nbt == null && regionfile == null) {
++ if (regionfile == null) {
++ // if the RegionFile doesn't exist, no point in deleting from it
+ return;
+ }
-+ try { // Try finally to unlock the region file
+ // Paper end - rewrite chunk system
// Paper start - Chunk save reattempt
int attempts = 0;
Exception lastException = null;
-@@ -179,9 +257,14 @@ public class RegionFileStorage implements AutoCloseable {
- net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk {}", pos, lastException);
- }
- // Paper end - Chunk save reattempt
+@@ -182,30 +277,37 @@ public final class RegionFileStorage implements AutoCloseable {
+ }
+
+ public void close() throws IOException {
+- ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
+- ObjectIterator objectiterator = this.regionCache.values().iterator();
+-
+- while (objectiterator.hasNext()) {
+- RegionFile regionfile = (RegionFile) objectiterator.next();
+-
+- try {
+- regionfile.close();
+- } catch (IOException ioexception) {
+- exceptionsuppressor.add(ioexception);
+ // Paper start - rewrite chunk system
-+ } finally {
-+ regionfile.fileLock.unlock();
++ synchronized (this) {
++ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
++ for (final RegionFile regionFile : this.regionCache.values()) {
++ try {
++ regionFile.close();
++ } catch (final IOException ex) {
++ exceptionCollector.add(ex);
++ }
+ }
+- }
+
+- exceptionsuppressor.throwIfPresent();
++ exceptionCollector.throwIfPresent();
+ }
+ // Paper end - rewrite chunk system
}
-- public void close() throws IOException {
-+ public synchronized void close() throws IOException { // Paper -> synchronized
- ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
- ObjectIterator objectiterator = this.regionCache.values().iterator();
+ public void flush() throws IOException {
+- ObjectIterator objectiterator = this.regionCache.values().iterator();
+-
+- while (objectiterator.hasNext()) {
+- RegionFile regionfile = (RegionFile) objectiterator.next();
++ // Paper start - rewrite chunk system
++ synchronized (this) {
++ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
++ for (final RegionFile regionFile : this.regionCache.values()) {
++ try {
++ regionFile.flush();
++ } catch (final IOException ex) {
++ exceptionCollector.add(ex);
++ }
++ }
+
+- regionfile.flush();
++ exceptionCollector.throwIfPresent();
+ }
++ // Paper end - rewrite chunk system
-@@ -198,7 +281,7 @@ public class RegionFileStorage implements AutoCloseable {
- exceptionsuppressor.throwIfPresent();
}
-- public void flush() throws IOException {
-+ public synchronized void flush() throws IOException { // Paper - synchronize
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-index 151fcbca34e02783e19fbb7b54ec4fbec2eed190..883fbe5c81e3be27007a1a0489f80ba1863e5a04 100644
+index 092773bd39d77a0dbe22db97c11aecb4a297111c..c7ed3eb80f6e8b918434153093644776866aa220 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
-@@ -12,6 +12,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
- import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
- import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
- import java.io.IOException;
-+import java.nio.file.Path;
- import java.util.Map;
- import java.util.Optional;
- import java.util.concurrent.CompletableFuture;
-@@ -31,25 +32,30 @@ import net.minecraft.world.level.ChunkPos;
+@@ -31,10 +31,10 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import org.slf4j.Logger;
-public class SectionStorage<R> implements AutoCloseable {
-+public class SectionStorage<R> extends RegionFileStorage implements AutoCloseable { // Paper - nuke IOWorker
++public abstract class SectionStorage<R> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private static final String SECTIONS_TAG = "Sections";
- private final SimpleRegionStorage simpleRegionStorage;
-+ // Paper - remove mojang I/O thread
++ // Paper - rewrite chunk system
private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>();
private final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet();
private final Function<Runnable, Codec<R>> codec;
- private final Function<Runnable, R> factory;
-- private final RegistryAccess registryAccess;
-+ public final RegistryAccess registryAccess; // Paper - rewrite chunk system - public
+@@ -43,6 +43,15 @@ public class SectionStorage<R> implements AutoCloseable {
+ private final ChunkIOErrorReporter errorReporter;
protected final LevelHeightAccessor levelHeightAccessor;
++ // Paper start - rewrite chunk system
++ private final RegionFileStorage regionStorage;
++
++ @Override
++ public final RegionFileStorage moonrise$getRegionStorage() {
++ return this.regionStorage;
++ }
++ // Paper end - rewrite chunk system
++
public SectionStorage(
-+ // Paper start
-+ RegionStorageInfo regionStorageInfo,
-+ Path path,
-+ boolean dsync,
-+ // Paper end
SimpleRegionStorage storageAccess,
Function<Runnable, Codec<R>> codecFactory,
- Function<Runnable, R> factory,
- RegistryAccess registryManager,
+@@ -51,12 +60,13 @@ public class SectionStorage<R> implements AutoCloseable {
+ ChunkIOErrorReporter errorHandler,
LevelHeightAccessor world
) {
- this.simpleRegionStorage = storageAccess;
-+ super(regionStorageInfo, path, dsync); // Paper - remove mojang I/O thread
++ // Paper - rewrite chunk system
this.codec = codecFactory;
this.factory = factory;
this.registryAccess = registryManager;
-@@ -112,23 +118,21 @@ public class SectionStorage<R> implements AutoCloseable {
+ this.errorReporter = errorHandler;
+ this.levelHeightAccessor = world;
++ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system
}
- private void readColumn(ChunkPos pos) {
-- Optional<CompoundTag> optional = this.tryRead(pos).join();
-- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE);
-- this.readColumn(pos, registryOps, optional.orElse(null));
-+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
+ protected void tick(BooleanSupplier shouldKeepTicking) {
+@@ -121,44 +131,17 @@ public class SectionStorage<R> implements AutoCloseable {
}
private CompletableFuture<Optional<CompoundTag>> tryRead(ChunkPos pos) {
- return this.simpleRegionStorage.read(pos).exceptionally(throwable -> {
- if (throwable instanceof IOException iOException) {
- LOGGER.error("Error reading chunk {} data from disk", pos, iOException);
+- this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), pos);
- return Optional.empty();
- } else {
- throw new CompletionException(throwable);
@@ -21112,58 +27882,97 @@ index 151fcbca34e02783e19fbb7b54ec4fbec2eed190..883fbe5c81e3be27007a1a0489f80ba1
- });
+ // Paper start - rewrite chunk system
+ try {
-+ return CompletableFuture.completedFuture(Optional.ofNullable(this.read(pos)));
-+ } catch (Throwable thr) {
++ return CompletableFuture.completedFuture(Optional.ofNullable(this.moonrise$read(pos.x, pos.z)));
++ } catch (final Throwable thr) {
+ return CompletableFuture.failedFuture(thr);
+ }
+ // Paper end - rewrite chunk system
}
private void readColumn(ChunkPos pos, RegistryOps<Tag> ops, @Nullable CompoundTag nbt) {
-+ if (true) throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
- if (nbt == null) {
- for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) {
- this.storage.put(getKey(pos, i), Optional.empty());
-@@ -138,7 +142,7 @@ public class SectionStorage<R> implements AutoCloseable {
- int j = getVersion(dynamic);
- int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
- boolean bl = j != k;
+- if (nbt == null) {
+- for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) {
+- this.storage.put(getKey(pos, i), Optional.empty());
+- }
+- } else {
+- Dynamic<Tag> dynamic = new Dynamic<>(ops, nbt);
+- int j = getVersion(dynamic);
+- int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
+- boolean bl = j != k;
- Dynamic<Tag> dynamic2 = this.simpleRegionStorage.upgradeChunkTag(dynamic, j);
-+ Dynamic<Tag> dynamic2 = null; // Paper - rewrite chunk system
- OptionalDynamic<Tag> optionalDynamic = dynamic2.get("Sections");
+- OptionalDynamic<Tag> optionalDynamic = dynamic2.get("Sections");
+-
+- for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) {
+- long m = getKey(pos, l);
+- Optional<R> optional = optionalDynamic.get(Integer.toString(l))
+- .result()
+- .flatMap(dynamicx -> this.codec.apply(() -> this.setDirty(m)).parse(dynamicx).resultOrPartial(LOGGER::error));
+- this.storage.put(m, optional);
+- optional.ifPresent(sections -> {
+- this.onSectionLoad(m);
+- if (bl) {
+- this.setDirty(m);
+- }
+- });
+- }
+- }
++ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
+ }
- for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) {
-@@ -162,7 +166,7 @@ public class SectionStorage<R> implements AutoCloseable {
+ private void writeColumn(ChunkPos pos) {
+@@ -166,10 +149,13 @@ public class SectionStorage<R> implements AutoCloseable {
Dynamic<Tag> dynamic = this.writeColumn(pos, registryOps);
Tag tag = dynamic.getValue();
if (tag instanceof CompoundTag) {
-- this.simpleRegionStorage.write(pos, (CompoundTag)tag);
-+ try { this.write(pos, (CompoundTag)tag); } catch (IOException ex) { SectionStorage.LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex); } // Paper - nuke IOWorker
+- this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> {
+- this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos);
+- return null;
+- });
++ // Paper start - rewrite chunk system
++ try {
++ this.moonrise$write(pos.x, pos.z, (net.minecraft.nbt.CompoundTag)tag);
++ } catch (final IOException ex) {
++ LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex);
++ }
++ // Paper end - rewrite chunk system
} else {
LOGGER.error("Expected compound tag, got {}", tag);
}
-@@ -212,7 +216,7 @@ public class SectionStorage<R> implements AutoCloseable {
- }
-
- private static int getVersion(Dynamic<?> dynamic) {
-- return dynamic.get("DataVersion").asInt(1945);
-+ return dynamic.get("DataVersion").asInt(1945); // Paper - diff on change, constant used in ChunkLoadTask
+@@ -209,7 +195,7 @@ public class SectionStorage<R> implements AutoCloseable {
+ protected void onSectionLoad(long pos) {
}
- public void flush(ChunkPos pos) {
-@@ -229,6 +233,6 @@ public class SectionStorage<R> implements AutoCloseable {
+- protected void setDirty(long pos) {
++ public void setDirty(long pos) { // Paper - public
+ Optional<R> optional = this.storage.get(pos);
+ if (optional != null && !optional.isEmpty()) {
+ this.dirty.add(pos);
+@@ -236,6 +222,6 @@ public class SectionStorage<R> implements AutoCloseable {
@Override
public void close() throws IOException {
- this.simpleRegionStorage.close();
-+ super.close(); // Paper - nuke I/O worker - don't call the worker
++ this.moonrise$close(); // Paper - rewrite chunk system
}
}
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+index e0e843f4f69013379ed70cb63d9b4f72163b828b..aafb05c5e63903f5790a6bcb862c8d79588be5a6 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+@@ -14,7 +14,7 @@ import net.minecraft.util.datafix.DataFixTypes;
+ import net.minecraft.world.level.ChunkPos;
+
+ public class SimpleRegionStorage implements AutoCloseable {
+- private final IOWorker worker;
++ public final IOWorker worker; // Paper - public
+ private final DataFixer fixerUpper;
+ private final DataFixTypes dataFixType;
+
diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386ddee4feef 100644
+index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644
--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
-@@ -9,52 +9,41 @@ import javax.annotation.Nullable;
+@@ -9,52 +9,38 @@ import javax.annotation.Nullable;
import net.minecraft.world.entity.Entity;
public class EntityTickList {
@@ -21171,7 +27980,7 @@ index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386d
- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>();
- @Nullable
- private Int2ObjectMap<Entity> iterated;
-+ private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<Entity> entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking?
++ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<net.minecraft.world.entity.Entity> entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system
private void ensureActiveIsNotIterated() {
- if (this.iterated == this.active) {
@@ -21185,26 +27994,24 @@ index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386d
- this.active = this.passive;
- this.passive = int2ObjectMap;
- }
-+ // Paper - replace with better logic, do not delay removals
++ // Paper - rewrite chunk system
}
public void add(Entity entity) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper
this.ensureActiveIsNotIterated();
- this.active.put(entity.getId(), entity);
-+ this.entities.add(entity); // Paper - replace with better logic, do not delay removals/additions
++ this.entities.add(entity); // Paper - rewrite chunk system
}
public void remove(Entity entity) {
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper
this.ensureActiveIsNotIterated();
- this.active.remove(entity.getId());
-+ this.entities.remove(entity); // Paper - replace with better logic, do not delay removals/additions
++ this.entities.remove(entity); // Paper - rewrite chunk system
}
public boolean contains(Entity entity) {
- return this.active.containsKey(entity.getId());
-+ return this.entities.contains(entity); // Paper - replace with better logic, do not delay removals/additions
++ return this.entities.contains(entity); // Paper - rewrite chunk system
}
public void forEach(Consumer<Entity> action) {
@@ -21219,11 +28026,10 @@ index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386d
- }
- } finally {
- this.iterated = null;
-+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper
-+ // Paper start - replace with better logic, do not delay removals/additions
++ // Paper start - rewrite chunk system
+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
+ // (by dfl iterator() is configured to not iterate over new entries)
-+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
++ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
+ try {
+ while (iterator.hasNext()) {
+ action.accept(iterator.next());
@@ -21231,278 +28037,407 @@ index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386d
+ } finally {
+ iterator.finishedIterating();
}
-+ // Paper end - replace with better logic, do not delay removals/additions
++ // Paper end - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-index 5d15c228c044a36c67014793decb314240cf6be1..dc765b92cc90f5f370254e68bbbdfa5add7935ce 100644
+index fb0be805c86e311927f55e8f090592465195384e..996899cb18e6c29665b9de7a1cc97c9a4187924b 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-@@ -87,7 +87,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+@@ -86,7 +86,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk);
return chunk;
- }), Util.backgroundExecutor());
-+ }), executor); // Paper - run with supplied executor
++ }), Runnable::run); // Paper - rewrite chunk system
}
private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) {
-@@ -286,7 +286,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+@@ -311,7 +311,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+ }
- return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("wgen_fill_noise", () -> {
- return this.doFill(blender, structureAccessor, noiseConfig, chunk, j, k);
-- }), Util.backgroundExecutor()).whenCompleteAsync((ichunkaccess1, throwable) -> {
-+ }), executor).whenCompleteAsync((ichunkaccess1, throwable) -> { // Paper - run with supplied executor
- Iterator iterator = set.iterator();
+ return ichunkaccess1;
+- }), Util.backgroundExecutor());
++ }), Runnable::run); // Paper - rewrite chunk system
+ }
- while (iterator.hasNext()) {
+ private ChunkAccess doFill(Blender blender, StructureManager structureAccessor, RandomState noiseConfig, ChunkAccess chunk, int minimumCellY, int cellHeight) {
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-index 609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93..7068657b28a9bc175ee23f5a18defb41168f1d76 100644
+index c6181e14d85d454506534f9bbe856156c0d4a062..3694c5d2d522216cd2e6e91e502a56a08595ca84 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -47,8 +47,101 @@ public class StructureCheck {
+@@ -47,8 +47,13 @@ public class StructureCheck {
private final BiomeSource biomeSource;
private final long seed;
private final DataFixer fixerUpper;
- private final Long2ObjectMap<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>();
- private final Map<Structure, Long2BooleanMap> featureChecks = new HashMap<>();
-+ // Paper start - rewrite chunk system - synchronise this class
-+ // additionally, make sure to purge entries from the maps so it does not leak memory
++ // Paper start - rewrite chunk system
++ // make sure to purge entries from the maps to prevent memory leaks
+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
-+
-+ private final SynchronisedLong2ObjectMap<Object2IntMap<Structure>> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
-+ private final java.util.concurrent.ConcurrentHashMap<Structure, SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
-+
-+ private static final class SynchronisedLong2ObjectMap<V> {
-+ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<V> map = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>();
-+ private final int limit;
-+
-+ public SynchronisedLong2ObjectMap(final int limit) {
-+ this.limit = limit;
-+ }
-+
-+ // must hold lock on map
-+ private void purgeEntries() {
-+ while (this.map.size() > this.limit) {
-+ this.map.removeLast();
-+ }
-+ }
-+
-+ public V get(final long key) {
-+ synchronized (this.map) {
-+ return this.map.getAndMoveToFirst(key);
-+ }
-+ }
-+
-+ public V put(final long key, final V value) {
-+ synchronized (this.map) {
-+ final V ret = this.map.putAndMoveToFirst(key, value);
-+ this.purgeEntries();
-+ return ret;
-+ }
-+ }
-+
-+ public V compute(final long key, final java.util.function.BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
-+ synchronized (this.map) {
-+ // first, compute the value - if one is added, it will be at the last entry
-+ this.map.compute(key, remappingFunction);
-+ // move the entry to first, just in case it was added at last
-+ final V ret = this.map.getAndMoveToFirst(key);
-+ // now purge the last entries
-+ this.purgeEntries();
-+
-+ return ret;
-+ }
-+ }
-+ }
-+
-+ private static final class SynchronisedLong2BooleanMap {
-+ private final it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap();
-+ private final int limit;
-+
-+ public SynchronisedLong2BooleanMap(final int limit) {
-+ this.limit = limit;
-+ }
-+
-+ // must hold lock on map
-+ private void purgeEntries() {
-+ while (this.map.size() > this.limit) {
-+ this.map.removeLastBoolean();
-+ }
-+ }
-+
-+ public boolean remove(final long key) {
-+ synchronized (this.map) {
-+ return this.map.remove(key);
-+ }
-+ }
-+
-+ // note:
-+ public boolean getOrCompute(final long key, final it.unimi.dsi.fastutil.longs.Long2BooleanFunction ifAbsent) {
-+ synchronized (this.map) {
-+ if (this.map.containsKey(key)) {
-+ return this.map.getAndMoveToFirst(key);
-+ }
-+ }
-+
-+ final boolean put = ifAbsent.get(key);
-+
-+ synchronized (this.map) {
-+ if (this.map.containsKey(key)) {
-+ return this.map.getAndMoveToFirst(key);
-+ }
-+ this.map.putAndMoveToFirst(key, put);
-+
-+ this.purgeEntries();
-+
-+ return put;
-+ }
-+ }
-+ }
-+ // Paper end - rewrite chunk system - synchronise this class
++ private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<it.unimi.dsi.fastutil.objects.Object2IntMap<Structure>> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
++ private final java.util.concurrent.ConcurrentHashMap<Structure, ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
++ // Paper end - rewrite chunk system
public StructureCheck(
ChunkScanAccess chunkIoWorker,
-@@ -90,7 +183,7 @@ public class StructureCheck {
+@@ -90,7 +95,7 @@ public class StructureCheck {
public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
long l = pos.toLong();
- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
-+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system - synchronise this class
++ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system
if (object2IntMap != null) {
return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
} else {
-@@ -100,9 +193,9 @@ public class StructureCheck {
+@@ -100,9 +105,11 @@ public class StructureCheck {
} else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
return StructureCheckResult.START_NOT_PRESENT;
} else {
- boolean bl = this.featureChecks
- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap())
- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type));
-+ boolean bl = this.featureChecksSafe // Paper - rewrite chunk system - synchronise this class
-+ .computeIfAbsent(type, structure2 -> new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) // Paper - rewrite chunk system - synchronise this class
-+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type)); // Paper - rewrite chunk system - synchronise this class
++ // Paper start - rewrite chunk system
++ boolean bl = this.featureChecksSafe
++ .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
++ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type));
++ // Paper end - rewrite chunk system
return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
}
}
-@@ -228,15 +321,26 @@ public class StructureCheck {
+@@ -228,15 +235,25 @@ public class StructureCheck {
}
private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) {
- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure));
- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos));
-+ // Paper start - rewrite chunk system - synchronise this class
++ // Paper start - rewrite chunk system
+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure));
+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything
+ // from this map, as everything that checks this map uses loadedChunks first
+ // so, one way or another it's a race condition that doesn't matter
-+ for (SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
++ for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
+ value.remove(pos);
+ }
-+ // Paper end - rewrite chunk system - synchronise this class
++ // Paper end - rewrite chunk system
}
public void incrementReference(ChunkPos pos, Structure structure) {
- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> {
- if (referencesByStructure == null || referencesByStructure.isEmpty()) {
-+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system - synchronise this class
-+ // make this COW so that we do not mutate state that may be currently in use
++ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system
+ if (referencesByStructure == null) {
referencesByStructure = new Object2IntOpenHashMap<>();
+ } else {
+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
}
-+ // Paper end - rewrite chunk system - synchronise this class
++ // Paper end - rewrite chunk system
referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1);
return referencesByStructure;
+diff --git a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
+index 82e4fad11121167445df97060fb717fa86191297..b3e2bb9245be1bb2f587117b0f6016cba18e217f 100644
+--- a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
++++ b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
+@@ -9,145 +9,103 @@ import net.minecraft.world.level.LightLayer;
+ import net.minecraft.world.level.chunk.DataLayer;
+ import net.minecraft.world.level.chunk.LightChunkGetter;
+
+-public class LevelLightEngine implements LightEventListener {
++public class LevelLightEngine implements LightEventListener, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider {
+ public static final int LIGHT_SECTION_PADDING = 1;
+ protected final LevelHeightAccessor levelHeightAccessor;
+- @Nullable
+- private final LightEngine<?, ?> blockEngine;
+- @Nullable
+- private final LightEngine<?, ?> skyEngine;
++ // Paper start - rewrite chunk system
++ protected final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface lightEngine;
++
++ @Override
++ public final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface starlight$getLightEngine() {
++ return this.lightEngine;
++ }
++
++ @Override
++ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
++ final DataLayer nibble, final boolean trustEdges) {
++ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
++ }
++
++ @Override
++ public void starlight$clientRemoveLightData(final ChunkPos chunkPos) {
++ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
++ }
++
++ @Override
++ public void starlight$clientChunkLoad(final ChunkPos pos, final net.minecraft.world.level.chunk.LevelChunk chunk) {
++ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
++ }
++ // Paper end - rewrite chunk system
+
+ public LevelLightEngine(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) {
+ this.levelHeightAccessor = chunkProvider.getLevel();
+- this.blockEngine = hasBlockLight ? new BlockLightEngine(chunkProvider) : null;
+- this.skyEngine = hasSkyLight ? new SkyLightEngine(chunkProvider) : null;
++ // Paper start - rewrite chunk system
++ if (chunkProvider.getLevel() instanceof net.minecraft.world.level.Level) {
++ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
++ } else {
++ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
++ }
++ // Paper end - rewrite chunk system
+ }
+
+ @Override
+ public void checkBlock(BlockPos pos) {
+- if (this.blockEngine != null) {
+- this.blockEngine.checkBlock(pos);
+- }
+-
+- if (this.skyEngine != null) {
+- this.skyEngine.checkBlock(pos);
+- }
++ this.lightEngine.blockChange(pos.immutable()); // Paper - rewrite chunk system
+ }
+
+ @Override
+ public boolean hasLightWork() {
+- return this.skyEngine != null && this.skyEngine.hasLightWork() || this.blockEngine != null && this.blockEngine.hasLightWork();
++ return this.lightEngine.hasUpdates(); // Paper - rewrite chunk system
+ }
+
+ @Override
+ public int runLightUpdates() {
+- int i = 0;
+- if (this.blockEngine != null) {
+- i += this.blockEngine.runLightUpdates();
+- }
+-
+- if (this.skyEngine != null) {
+- i += this.skyEngine.runLightUpdates();
+- }
+-
+- return i;
++ final boolean hadUpdates = this.hasLightWork();
++ this.lightEngine.propagateChanges();
++ return hadUpdates ? 1 : 0; // Paper - rewrite chunk system
+ }
+
+ @Override
+ public void updateSectionStatus(SectionPos pos, boolean notReady) {
+- if (this.blockEngine != null) {
+- this.blockEngine.updateSectionStatus(pos, notReady);
+- }
+-
+- if (this.skyEngine != null) {
+- this.skyEngine.updateSectionStatus(pos, notReady);
+- }
++ this.lightEngine.sectionChange(pos, notReady); // Paper - rewrite chunk system
+ }
+
+ @Override
+ public void setLightEnabled(ChunkPos pos, boolean retainData) {
+- if (this.blockEngine != null) {
+- this.blockEngine.setLightEnabled(pos, retainData);
+- }
+-
+- if (this.skyEngine != null) {
+- this.skyEngine.setLightEnabled(pos, retainData);
+- }
++ // Paper - rewrite chunk system
+ }
+
+ @Override
+ public void propagateLightSources(ChunkPos chunkPos) {
+- if (this.blockEngine != null) {
+- this.blockEngine.propagateLightSources(chunkPos);
+- }
+-
+- if (this.skyEngine != null) {
+- this.skyEngine.propagateLightSources(chunkPos);
+- }
++ // Paper - rewrite chunk system
+ }
+
+ public LayerLightEventListener getLayerListener(LightLayer lightType) {
+- if (lightType == LightLayer.BLOCK) {
+- return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine);
+- } else {
+- return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine);
+- }
++ return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
+ }
+
+ public String getDebugData(LightLayer lightType, SectionPos pos) {
+- if (lightType == LightLayer.BLOCK) {
+- if (this.blockEngine != null) {
+- return this.blockEngine.getDebugData(pos.asLong());
+- }
+- } else if (this.skyEngine != null) {
+- return this.skyEngine.getDebugData(pos.asLong());
+- }
+-
+- return "n/a";
++ return "n/a"; // Paper - rewrite chunk system
+ }
+
+ public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightType, SectionPos pos) {
+- if (lightType == LightLayer.BLOCK) {
+- if (this.blockEngine != null) {
+- return this.blockEngine.getDebugSectionType(pos.asLong());
+- }
+- } else if (this.skyEngine != null) {
+- return this.skyEngine.getDebugSectionType(pos.asLong());
+- }
+-
+- return LayerLightSectionStorage.SectionType.EMPTY;
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+ }
+
+ public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
+- if (lightType == LightLayer.BLOCK) {
+- if (this.blockEngine != null) {
+- this.blockEngine.queueSectionData(pos.asLong(), nibbles);
+- }
+- } else if (this.skyEngine != null) {
+- this.skyEngine.queueSectionData(pos.asLong(), nibbles);
+- }
++ // Paper - rewrite chunk system
+ }
+
+ public void retainData(ChunkPos pos, boolean retainData) {
+- if (this.blockEngine != null) {
+- this.blockEngine.retainData(pos, retainData);
+- }
+-
+- if (this.skyEngine != null) {
+- this.skyEngine.retainData(pos, retainData);
+- }
++ // Paper - rewrite chunk system
+ }
+
+ public int getRawBrightness(BlockPos pos, int ambientDarkness) {
+- int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(pos) - ambientDarkness;
+- int j = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(pos);
+- return Math.max(j, i);
++ return this.lightEngine.getRawBrightness(pos, ambientDarkness); // Paper - rewrite chunk system
+ }
+
+ public boolean lightOnInSection(SectionPos sectionPos) {
+- long l = sectionPos.asLong();
+- return this.blockEngine == null
+- || this.blockEngine.storage.lightOnInSection(l) && (this.skyEngine == null || this.skyEngine.storage.lightOnInSection(l));
++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server
+ }
+
+ public int getLightSectionCount() {
diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
-index 47c2b2da9799690291396effb9e1b06d71efc6fd..2cdd18f724296f10cd4a522d1e8196723d39cf45 100644
+index 47c2b2da9799690291396effb9e1b06d71efc6fd..c42c0d1e4da30aa15f32d4ca524aeabd26fc50cf 100644
--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
+++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
-@@ -26,6 +26,19 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -18,7 +18,7 @@ import net.minecraft.core.BlockPos;
+ import net.minecraft.nbt.ListTag;
+ import net.minecraft.world.level.ChunkPos;
+
+-public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T> {
++public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system
+ private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER);
+ @Nullable
+ private List<SavedTick<T>> pendingTicks;
+@@ -26,6 +26,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Nullable
private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded;
-+ // Paper start - add dirty flag
++ // Paper start - rewrite chunk system
++ /*
++ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks
++ * and the last saved tick is not equal to the current tick
++ */
++ /*
++ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a
++ * bit out of scope for the chunk system
++ */
++
+ private boolean dirty;
+ private long lastSaved = Long.MIN_VALUE;
+
-+ public boolean isDirty(final long tick) {
++ @Override
++ public final boolean moonrise$isDirty(final long tick) {
+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved);
+ }
+
-+ public void clearDirty() {
++ @Override
++ public final void moonrise$clearDirty() {
+ this.dirty = false;
+ }
-+ // Paper end - add dirty flag
++ // Paper end - rewrite chunk system
+
public LevelChunkTicks() {
}
-@@ -50,6 +63,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -50,7 +74,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public ScheduledTick<T> poll() {
ScheduledTick<T> scheduledTick = this.tickQueue.poll();
if (scheduledTick != null) {
-+ this.dirty = true; // Paper - add dirty flag
- this.ticksPerPosition.remove(scheduledTick);
+- this.ticksPerPosition.remove(scheduledTick);
++ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system
}
-@@ -59,6 +73,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+ return scheduledTick;
+@@ -59,7 +83,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public void schedule(ScheduledTick<T> orderedTick) {
if (this.ticksPerPosition.add(orderedTick)) {
-+ this.dirty = true; // Paper - add dirty flag
- this.scheduleUnchecked(orderedTick);
+- this.scheduleUnchecked(orderedTick);
++ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system
}
}
-@@ -81,7 +96,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+
+@@ -81,7 +105,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
while (iterator.hasNext()) {
ScheduledTick<T> scheduledTick = iterator.next();
if (predicate.test(scheduledTick)) {
- iterator.remove();
-+ iterator.remove(); this.dirty = true; // Paper - add dirty flag
++ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system
this.ticksPerPosition.remove(scheduledTick);
}
}
-@@ -98,6 +113,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -98,6 +122,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public ListTag save(long l, Function<T, String> function) {
-+ this.lastSaved = l; // Paper - add dirty system to level ticks
++ this.lastSaved = l; // Paper - rewrite chunk system
ListTag listTag = new ListTag();
if (this.pendingTicks != null) {
for (SavedTick<T> savedTick : this.pendingTicks) {
-@@ -114,6 +130,11 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
+@@ -114,6 +139,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public void unpack(long time) {
if (this.pendingTicks != null) {
-+ // Paper start - add dirty system to level chunk ticks
-+ if (this.tickQueue.isEmpty()) {
-+ this.lastSaved = time;
-+ }
-+ // Paper end - add dirty system to level chunk ticks
++ this.lastSaved = time; // Paper - rewrite chunk system
int i = -this.pendingTicks.size();
for (SavedTick<T> savedTick : this.pendingTicks) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-index 36a611d06131be00197c915871b8323544bb4972..bb22473df13f68ac3b45a9c000d1de7260e07792 100644
+index 69c7fe5bf5b914276a9f7a0e57ce668e569d91f9..cce2fed2d4e9d6147ea1854321012c6950eb05cc 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
-@@ -116,7 +116,7 @@ public class CraftChunk implements Chunk {
+@@ -116,60 +116,12 @@ public class CraftChunk implements Chunk {
@Override
public boolean isEntitiesLoaded() {
- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
-+ return this.getCraftWorld().getHandle().areEntitiesLoaded(io.papermc.paper.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
++ return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
}
@Override
-@@ -125,51 +125,7 @@ public class CraftChunk implements Chunk {
- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
- }
-
+ public Entity[] getEntities() {
+- if (!this.isLoaded()) {
+- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
+- }
+-
- PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
- long pair = ChunkPos.asLong(this.x, this.z);
-
@@ -21553,27 +28488,28 @@ index 36a611d06131be00197c915871b8323544bb4972..bb22473df13f68ac3b45a9c000d1de72
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-index 16a736e8327450712630b1659b156da879a57352..0e30a227948464979e12c991b10bd00cf7320399 100644
+index 77f3ac4e45a691181a94831cf49f7840c9f88e3a..05e44a1448f30ceb8cecba2bed76f51aac5543f9 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
-@@ -1426,7 +1426,6 @@ public final class CraftServer implements Server {
+@@ -1419,7 +1419,7 @@ public final class CraftServer implements Server {
// Paper - Put world into worldlist before initing the world; move up
this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
++ // Paper - rewrite chunk system
this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
return internal.getWorld();
-@@ -1471,7 +1470,7 @@ public final class CraftServer implements Server {
+@@ -1464,7 +1464,7 @@ public final class CraftServer implements Server {
}
handle.getChunkSource().close(save);
- handle.entityManager.close(save); // SPIGOT-6722: close entityManager
-+ // handle.entityManager.close(save); // SPIGOT-6722: close entityManager // Paper - rewrite chunk system
++ // Paper - rewrite chunk system
handle.convertable.close();
} catch (Exception ex) {
this.getLogger().log(Level.SEVERE, null, ex);
-@@ -2507,7 +2506,7 @@ public final class CraftServer implements Server {
+@@ -2500,7 +2500,7 @@ public final class CraftServer implements Server {
@Override
public boolean isPrimaryThread() {
@@ -21583,43 +28519,43 @@ index 16a736e8327450712630b1659b156da879a57352..0e30a227948464979e12c991b10bd00c
// Paper start - Adventure
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 7aee9f6b143c89cf8d65ca55eeda808152b4dd26..9c06c3729b09726e1da6ff8fb975cef2aeba9643 100644
+index 8f88ccec6b8947ca2738dc07c23aebe258145c83..cdc704364cf339084537d089e654f6078f8be783 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -518,10 +518,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -456,10 +456,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
if (playerChunk == null) return false;
- playerChunk.getTickingChunkFuture().thenAccept(either -> {
- either.ifSuccess(chunk -> {
-+ // Paper start - rewrite player chunk loader
-+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getSendingChunk();
++ // Paper start - chunk system
++ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
+ if (chunk == null) {
+ return false;
+ }
-+ // Paper end - rewrite player chunk loader
++ // Paper end - chunk system
List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
- if (playersInRange.isEmpty()) return;
-+ if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader
++ if (playersInRange.isEmpty()) return true; // Paper - chunk system
ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null);
for (ServerPlayer player : playersInRange) {
-@@ -529,8 +533,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -467,8 +471,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
player.connection.send(refreshPacket);
}
- });
- });
-+ // Paper - rewrite player chunk loader
++ // Paper - chunk system
return true;
}
-@@ -634,20 +637,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -572,20 +575,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Collection<Plugin> getPluginChunkTickets(int x, int z) {
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
--
+
- if (tickets == null) {
- return Collections.emptyList();
- }
@@ -21636,31 +28572,31 @@ index 7aee9f6b143c89cf8d65ca55eeda808152b4dd26..9c06c3729b09726e1da6ff8fb975cef2
}
@Override
-@@ -655,7 +645,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -593,7 +584,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
-+ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
++ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
long chunkKey = chunkTickets.getLongKey();
SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
-@@ -1352,12 +1342,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -1290,12 +1281,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public int getViewDistance() {
- return this.world.getChunkSource().chunkMap.serverViewDistance;
-+ return this.getHandle().playerChunkLoader.getAPIViewDistance(); // Paper - replace player chunk loader
++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
}
@Override
public int getSimulationDistance() {
- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
-+ return this.getHandle().playerChunkLoader.getAPITickDistance(); // Paper - replace player chunk loader
++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
}
public BlockMetadataStore getBlockMetadata() {
-@@ -2520,17 +2510,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+@@ -2433,17 +2424,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void setSimulationDistance(final int simulationDistance) {
@@ -21668,150 +28604,113 @@ index 7aee9f6b143c89cf8d65ca55eeda808152b4dd26..9c06c3729b09726e1da6ff8fb975cef2
+ if (simulationDistance < 2 || simulationDistance > 32) {
+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
+ }
-+ this.getHandle().chunkSource.chunkMap.setTickViewDistance(simulationDistance);
++ this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system
}
@Override
public int getSendViewDistance() {
- return this.getViewDistance();
-+ return this.getHandle().playerChunkLoader.getAPISendViewDistance(); // Paper - replace player chunk loader
++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
}
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
-+ this.getHandle().chunkSource.chunkMap.setSendViewDistance(viewDistance); // Paper - replace player chunk loader
++ this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system
}
// Paper start - implement pointers
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index 68a0b6b8650e9e80e8e8c4037d92389cae899d72..9aec6efef4094bbdb920101df1a7a5a2a6070dde 100644
+index 67a715a812d700df912834874107078255d7c695..545921a7e07dccc749711208f160fdbfab53c5fa 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-@@ -3463,31 +3463,31 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+@@ -3490,12 +3490,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public int getViewDistance() {
- return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle());
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPIViewDistance(this);
++ return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1; // Paper - rewrite chunk system - TODO do this better
}
@Override
public void setViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
-+ this.getHandle().setLoadViewDistance(viewDistance < 0 ? viewDistance : viewDistance + 1);
++ // Paper - rewrite chunk system - TODO do this better
++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
++ .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1);
}
@Override
- public int getSimulationDistance() {
-- return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle());
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(this);
- }
+@@ -3505,7 +3507,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSimulationDistance(final int simulationDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
-+ this.getHandle().setTickViewDistance(simulationDistance);
++ // Paper - rewrite chunk system - TODO do this better
++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
++ .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance);
}
@Override
- public int getSendViewDistance() {
-- return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle());
-+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(this);
- }
+@@ -3515,6 +3519,8 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
-+ this.getHandle().setSendViewDistance(viewDistance);
++ // Paper - rewrite chunk system - TODO do this better
++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
++ .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-index b65710b648e31ab74204b5abd9397d9e6e26dac4..c77f722131e0e40e9de29bf8d42f9bc5d8fa2f7d 100644
+index 5717c0e1d6df07a4613356dc78d970d2101c68d7..cab7ca4218e5903b6a5e518af55457b9a1b5111c 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-@@ -264,7 +264,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
+@@ -263,7 +263,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
return ichunkaccess1;
};
- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
-+ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), executor) : future.thenApply(function); // Paper - run with supplied executor
++ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-index cd7f1309cf01a5f01a28aded03a36fe15adb1756..41a291d42667c38d3e5bbe47236772761e85929b 100644
+index cd7f1309cf01a5f01a28aded03a36fe15adb1756..43cb7b91945a72ab4bd998a3a3eca3cdcf0432a9 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-@@ -815,19 +815,39 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
+@@ -815,19 +815,26 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
@Nullable
@Override
public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
- return null;
-+ return this.handle.getBlockStateIfLoaded(blockposition);
++ return this.handle.getBlockStateIfLoaded(blockposition); // Paper - rewrite chunk system
}
@Nullable
@Override
public FluidState getFluidIfLoaded(final BlockPos blockposition) {
- return null;
-+ return this.handle.getFluidIfLoaded(blockposition);
++ return this.handle.getFluidIfLoaded(blockposition); // Paper - rewrite chunk system
}
@Nullable
@Override
public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
- return null;
-+ return this.handle.getChunkIfLoadedImmediately(x, z);
-+ }
-+
-+ @Override
-+ public void getHardCollidingEntities(final Entity except, final AABB box, final Predicate<? super Entity> predicate, final List<Entity> into) {
-+ this.handle.getHardCollidingEntities(except, box, predicate, into);
-+ }
-+
-+ @Override
-+ public List<Entity> getHardCollidingEntities(final Entity except, final AABB box, final Predicate<? super Entity> predicate) {
-+ return this.handle.getHardCollidingEntities(except, box, predicate);
-+ }
++ return this.handle.getChunkIfLoadedImmediately(x, z); // Paper - rewrite chunk system
+ }
+
++ // Paper start - rewrite chunk system
+ @Override
-+ public void getEntities(final Entity except, final AABB box, final Predicate<? super Entity> predicate, final List<Entity> into) {
-+ this.handle.getEntities(except, box, predicate, into);
++ public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) {
++ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate);
+ }
-+
-+ @Override
-+ public <T> void getEntitiesByClass(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, final Predicate<? super T> predicate) {
-+ this.handle.getEntitiesByClass(clazz, except, box, into, predicate);
- }
++ // Paper end - rewrite chunk system
// Paper end
}
-diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
-index e8a73d34dbb372581b03018aade170a31c266099..210f454a840aa5564f7cbf33b83d31aa74814c84 100644
---- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
-+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
-@@ -268,4 +268,19 @@ public class DummyGeneratorAccess implements WorldGenLevel {
- @Override
- public void scheduleTick(BlockPos pos, Fluid fluid, int delay, net.minecraft.world.ticks.TickPriority priority) {}
- // Paper end - add more methods
-+ // Paper start
-+ @Override
-+ public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
-+ return java.util.Collections.emptyList();
-+ }
-+
-+ @Override
-+ public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
-+
-+ @Override
-+ public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {}
-+
-+ @Override
-+ public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, AABB box, List<? super T> into, Predicate<? super T> predicate) {}
-+ // Paper end
- }
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
-index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..2e074c16dab1ead47914070329da0398c3274048 100644
+index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..67c8e90d3a2a93d858371d7fc1c3aaac3fdef71c 100644
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -9,7 +9,7 @@ public class AsyncCatcher
@@ -21819,28 +28718,19 @@ index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..2e074c16dab1ead47914070329da0398
public static void catchOp(String reason)
{
- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
-+ if (!(io.papermc.paper.util.TickThread.isTickThread())) // Paper
++ if (!io.papermc.paper.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
{
MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
throw new IllegalStateException( "Asynchronous " + reason + "!" );
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
-index ad282d34919716b75acd10426cd071da9d064a51..9e5d08f57aa448552d100ca892c211d44441ef68 100644
+index ad282d34919716b75acd10426cd071da9d064a51..c68256c0c8e131497108f677c6b254c589ce67e2 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -8,7 +8,7 @@ import java.util.logging.Logger;
- import net.minecraft.server.MinecraftServer;
- import org.bukkit.Bukkit;
-
--public class WatchdogThread extends Thread
-+public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system
- {
-
- private static WatchdogThread instance;
@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread
// Paper end - Different message for short timeout
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system
++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//
diff --git a/patches/server/0988-Rewrite-dataconverter-system.patch b/patches/server/0989-Rewrite-dataconverter-system.patch
index 8602229378..e28b6c8b61 100644
--- a/patches/server/0988-Rewrite-dataconverter-system.patch
+++ b/patches/server/0989-Rewrite-dataconverter-system.patch
@@ -29210,6 +29210,34 @@ index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98e
+ return correct.equals(value) ? null : correct;
+ }
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+index 49160a30b8e19e5c5ada811fbcae2a05959524f3..5fca5dc7cdfa976bcc58dfcf0d14abb78a931475 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
+@@ -25,13 +25,21 @@ public final class ChunkSystemConverters {
+ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+
+- return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
++ // Paper start - dataconverter
++ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(
++ ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.POI_CHUNK, data, dataVersion, getCurrentVersion()
++ );
++ // Paper end - dataconverter
+ }
+
+ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION);
+
+- return DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
++ // Paper start - dataconverter
++ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(
++ ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, data, dataVersion, getCurrentVersion()
++ );
++ // Paper end - dataconverter
+ }
+
+ private ChunkSystemConverters() {}
diff --git a/src/main/java/net/minecraft/data/structures/StructureUpdater.java b/src/main/java/net/minecraft/data/structures/StructureUpdater.java
index 6082d2a4dda21e1b1a9154629aaf0b282b154a42..d8fe611bb4e121dfe96ccd5b8e7949f91eeba023 100644
--- a/src/main/java/net/minecraft/data/structures/StructureUpdater.java
@@ -29224,10 +29252,10 @@ index 6082d2a4dda21e1b1a9154629aaf0b282b154a42..d8fe611bb4e121dfe96ccd5b8e7949f9
return structureTemplate.save(new CompoundTag());
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..278019416ce3be0ccec703c6a70c2a29f11e21b1 100644
+index 0cdc224656a2baa09b7dfbb249b6a96320ac43e0..8c270fee6ecdfbf2bf214428c6f7fcebc2087719 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -86,7 +86,7 @@ public class ChunkStorage implements AutoCloseable {
+@@ -96,7 +96,7 @@ public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patc
} else {
try {
// CraftBukkit start
@@ -29236,7 +29264,7 @@ index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..278019416ce3be0ccec703c6a70c2a29
CompoundTag level = nbttagcompound.getCompound("Level");
if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) {
ServerChunkCache cps = (generatoraccess == null) ? null : ((ServerLevel) generatoraccess).getChunkSource();
-@@ -98,7 +98,7 @@ public class ChunkStorage implements AutoCloseable {
+@@ -108,7 +108,7 @@ public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patc
// CraftBukkit end
if (i < 1493) {
@@ -29245,7 +29273,7 @@ index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..278019416ce3be0ccec703c6a70c2a29
if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
-@@ -116,7 +116,7 @@ public class ChunkStorage implements AutoCloseable {
+@@ -128,7 +128,7 @@ public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patc
// Spigot end
ChunkStorage.injectDatafixingContext(nbttagcompound, resourcekey, optional);
@@ -29255,7 +29283,7 @@ index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..278019416ce3be0ccec703c6a70c2a29
if (stopBelowZero) {
nbttagcompound.putString("Status", net.minecraft.core.registries.BuiltInRegistries.CHUNK_STATUS.getKey(ChunkStatus.SPAWN).toString());
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-index e0e843f4f69013379ed70cb63d9b4f72163b828b..578d270d5b7efb9ac8f5dde539170f6021e2b786 100644
+index aafb05c5e63903f5790a6bcb862c8d79588be5a6..c5085ebf4e801837010f3750c5e89576bb0c27a5 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
@@ -32,13 +32,30 @@ public class SimpleRegionStorage implements AutoCloseable {
@@ -29293,10 +29321,10 @@ index e0e843f4f69013379ed70cb63d9b4f72163b828b..578d270d5b7efb9ac8f5dde539170f60
public CompletableFuture<Void> synchronize(boolean sync) {
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-index c6181e14d85d454506534f9bbe856156c0d4a062..609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93 100644
+index 3694c5d2d522216cd2e6e91e502a56a08595ca84..9d282c263151c51cae84f8db00f6b8fa742fbad3 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -151,7 +151,7 @@ public class StructureCheck {
+@@ -158,7 +158,7 @@ public class StructureCheck {
CompoundTag compoundTag2;
try {
diff --git a/patches/server/0989-disable-forced-empty-world-ticks.patch b/patches/server/0990-disable-forced-empty-world-ticks.patch
index 7415c872ae..e33cae412e 100644
--- a/patches/server/0989-disable-forced-empty-world-ticks.patch
+++ b/patches/server/0990-disable-forced-empty-world-ticks.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] disable forced empty world ticks
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 4d7e234d379a451c4bb53bc2fcdf22cb191f8d1a..3f17212fc814156e2d3e8c7d4cf40680ab5cdbb5 100644
+index 37971d9fc59ecf3736fccf7a27f17e37a56efeb9..ec8eec86876221686f152bc5b25304cc59791cac 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -528,7 +528,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+@@ -697,7 +697,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
this.handlingTick = false;
gameprofilerfiller.pop();
diff --git a/patches/server/0990-stubs.patch b/patches/server/0991-stubs.patch
index 7f57021011..289a1338b8 100644
--- a/patches/server/0990-stubs.patch
+++ b/patches/server/0991-stubs.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] stubs
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index 67a715a812d700df912834874107078255d7c695..fc7fa260bb55ae28b17a3ea05682a51ad4fa39df 100644
+index 545921a7e07dccc749711208f160fdbfab53c5fa..ff2d05126d3857fa501f0b9df80e373b1811f7cf 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -110,6 +110,7 @@ import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
diff --git a/patches/unapplied/server/0993-Starlight.patch b/patches/unapplied/server/0993-Starlight.patch
deleted file mode 100644
index bc604e348c..0000000000
--- a/patches/unapplied/server/0993-Starlight.patch
+++ /dev/null
@@ -1,5429 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Wed, 28 Oct 2020 16:51:55 -0700
-Subject: [PATCH] Starlight
-
-See https://github.com/PaperMC/Starlight
-
-== AT ==
-public net.minecraft.server.level.ChunkHolder broadcast(Lnet/minecraft/network/protocol/Packet;Z)V
-
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3732a940d9603cf502983afbc4663113d1400be8
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java
-@@ -0,0 +1,275 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.chunk.PalettedContainer;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Set;
-+
-+public final class BlockStarLightEngine extends StarLightEngine {
-+
-+ public BlockStarLightEngine(final Level world) {
-+ super(false, world);
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setBlockEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getBlockNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setBlockNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
-+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
-+ // in increases, and thus the existing sky block check will actually correctly propagate light through
-+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
-+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
-+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
-+ // of vanilla data management we "hide" them.
-+ nibble.setHidden();
-+ }
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
-+ }
-+ } else {
-+ nibble.setNonNull();
-+ }
-+ }
-+
-+ @Override
-+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change emitted light
-+ // blocks can change direction of propagation
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
-+
-+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
-+ // this accounts for change in emitted light that would cause an increase
-+ if (emittedLevel != 0) {
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+ }
-+ // this also accounts for a change in emitted light that would cause a decrease
-+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
-+ // as it checks all neighbours (even if current level is 0)
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ // always keep sided transparent false here, new block might be conditionally transparent which would
-+ // prevent us from decreasing sources in the directions where the new block is opaque
-+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
-+ // catch that and fix it.
-+ );
-+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int level = centerState.getLightEmission() & 0xF;
-+
-+ if (level >= (15 - 1) || level > expect) {
-+ return level;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState conditionallyOpaqueState;
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ if (opacity == -1) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else if (opacity >= 15) {
-+ return level;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ opacity = Math.max(1, opacity);
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ // passed transparency,
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
-+ final List<BlockPos> sources = new ArrayList<>();
-+
-+ final int offX = chunk.getPos().x << 4;
-+ final int offZ = chunk.getPos().z << 4;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
-+ final LevelChunkSection section = sections[sectionY - this.minSection];
-+ if (section == null || section.hasOnlyAir()) {
-+ // no sources in empty sections
-+ continue;
-+ }
-+ if (!section.maybeHas((final BlockState state) -> {
-+ return state.getLightEmission() > 0;
-+ })) {
-+ // no light sources in palette
-+ continue;
-+ }
-+ final PalettedContainer<BlockState> states = section.states;
-+ final int offY = sectionY << 4;
-+
-+ for (int index = 0; index < (16 * 16 * 16); ++index) {
-+ final BlockState state = states.get(index);
-+ if (state.getLightEmission() <= 0) {
-+ continue;
-+ }
-+
-+ // index = x | (z << 4) | (y << 8)
-+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
-+ }
-+ }
-+
-+ return sources;
-+ }
-+
-+ @Override
-+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ // setup sources
-+ final int emittedMask = this.emittedLightMask;
-+ final List<BlockPos> positions = this.getSources(lightAccess, chunk);
-+ for (int i = 0, len = positions.size(); i < len; ++i) {
-+ final BlockPos pos = positions.get(i);
-+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+
-+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
-+ // some other source is brighter
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLight & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+
-+
-+ // propagation wont set this for us
-+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
-+ }
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ // verify neighbour edges
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ } else {
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4ffb4ffe01c4628d52742c5c0bbd35220eea6294
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java
-@@ -0,0 +1,440 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import net.minecraft.world.level.chunk.DataLayer;
-+import java.util.ArrayDeque;
-+import java.util.Arrays;
-+
-+// SWMR -> Single Writer Multi Reader Nibble Array
-+public final class SWMRNibbleArray {
-+
-+ /*
-+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
-+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
-+ * nibbles can be written to.
-+ *
-+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
-+ *
-+ * Initialised nibble - Has light data.
-+ */
-+
-+ protected static final int INIT_STATE_NULL = 0; // null
-+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
-+ protected static final int INIT_STATE_INIT = 2; // initialised
-+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
-+
-+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
-+ // this allows us to maintain only 1 byte array when we're not updating
-+ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
-+
-+ private static byte[] allocateBytes() {
-+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
-+ if (inPool != null) {
-+ return inPool;
-+ }
-+
-+ return new byte[ARRAY_SIZE];
-+ }
-+
-+ private static void freeBytes(final byte[] bytes) {
-+ WORKING_BYTES_POOL.get().addFirst(bytes);
-+ }
-+
-+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
-+ if (nibble == null) {
-+ return new SWMRNibbleArray(null, true);
-+ } else if (nibble.isEmpty()) {
-+ return new SWMRNibbleArray();
-+ } else {
-+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
-+ }
-+ }
-+
-+ protected int stateUpdating;
-+ protected volatile int stateVisible;
-+
-+ protected byte[] storageUpdating;
-+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
-+ protected volatile byte[] storageVisible;
-+
-+ public SWMRNibbleArray() {
-+ this(null, false); // lazy init
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes) {
-+ this(bytes, false);
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final int state) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
-+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
-+ }
-+ this.stateUpdating = this.stateVisible = state;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ StringBuilder stringBuilder = new StringBuilder();
-+ stringBuilder.append("State: ");
-+ switch (this.stateVisible) {
-+ case INIT_STATE_NULL:
-+ stringBuilder.append("null");
-+ break;
-+ case INIT_STATE_UNINIT:
-+ stringBuilder.append("uninitialised");
-+ break;
-+ case INIT_STATE_INIT:
-+ stringBuilder.append("initialised");
-+ break;
-+ case INIT_STATE_HIDDEN:
-+ stringBuilder.append("hidden");
-+ break;
-+ default:
-+ stringBuilder.append("unknown");
-+ break;
-+ }
-+ stringBuilder.append("\nData:\n");
-+
-+ final byte[] data = this.storageVisible;
-+ if (data != null) {
-+ for (int i = 0; i < 4096; ++i) {
-+ // Copied from NibbleArray#toString
-+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
-+
-+ stringBuilder.append(Integer.toHexString(level));
-+ if ((i & 15) == 15) {
-+ stringBuilder.append("\n");
-+ }
-+
-+ if ((i & 255) == 255) {
-+ stringBuilder.append("\n");
-+ }
-+ }
-+ } else {
-+ stringBuilder.append("null");
-+ }
-+
-+ return stringBuilder.toString();
-+ }
-+
-+ public SaveState getSaveState() {
-+ synchronized (this) {
-+ final int state = this.stateVisible;
-+ final byte[] data = this.storageVisible;
-+ if (state == INIT_STATE_NULL) {
-+ return null;
-+ }
-+ if (state == INIT_STATE_UNINIT) {
-+ return new SaveState(null, state);
-+ }
-+ final boolean zero = isAllZero(data);
-+ if (zero) {
-+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
-+ } else {
-+ return new SaveState(data.clone(), state);
-+ }
-+ }
-+ }
-+
-+ protected static boolean isAllZero(final byte[] data) {
-+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
-+ byte whole = data[i << 4];
-+
-+ for (int k = 1; k < (1 << 4); ++k) {
-+ whole |= data[(i << 4) | k];
-+ }
-+
-+ if (whole != 0) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: updating on src, updating on other
-+ public void extrudeLower(final SWMRNibbleArray other) {
-+ if (other.stateUpdating == INIT_STATE_NULL) {
-+ throw new IllegalArgumentException();
-+ }
-+
-+ if (other.storageUpdating == null) {
-+ this.setUninitialised();
-+ return;
-+ }
-+
-+ final byte[] src = other.storageUpdating;
-+ final byte[] into;
-+
-+ if (!this.updatingDirty) {
-+ if (this.storageUpdating != null) {
-+ into = this.storageUpdating = allocateBytes();
-+ } else {
-+ this.storageUpdating = into = allocateBytes();
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ } else {
-+ into = this.storageUpdating;
-+ }
-+
-+ final int start = 0;
-+ final int end = (15 | (15 << 4)) >>> 1;
-+
-+ /* x | (z << 4) | (y << 8) */
-+ for (int y = 0; y <= 15; ++y) {
-+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
-+ }
-+ }
-+
-+ // operation type: updating
-+ public void setFull() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setZero() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setNonNull() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_NULL) {
-+ return;
-+ }
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public void setNull() {
-+ this.stateUpdating = INIT_STATE_NULL;
-+ if (this.updatingDirty && this.storageUpdating != null) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setUninitialised() {
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ if (this.storageUpdating != null && this.updatingDirty) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setHidden() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_INIT) {
-+ this.setNull();
-+ } else {
-+ this.stateUpdating = INIT_STATE_HIDDEN;
-+ }
-+ }
-+
-+ // operation type: updating
-+ public boolean isDirty() {
-+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
-+ }
-+
-+ // operation type: updating
-+ public boolean isNullNibbleUpdating() {
-+ return this.stateUpdating == INIT_STATE_NULL;
-+ }
-+
-+ // operation type: visible
-+ public boolean isNullNibbleVisible() {
-+ return this.stateVisible == INIT_STATE_NULL;
-+ }
-+
-+ // opeartion type: updating
-+ public boolean isUninitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isUninitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isInitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isInitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenUpdating() {
-+ return this.stateUpdating == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenVisible() {
-+ return this.stateVisible == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ protected void swapUpdatingAndMarkDirty() {
-+ if (this.updatingDirty) {
-+ return;
-+ }
-+
-+ if (this.storageUpdating == null) {
-+ this.storageUpdating = allocateBytes();
-+ Arrays.fill(this.storageUpdating, (byte)0);
-+ } else {
-+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
-+ }
-+
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public boolean updateVisible() {
-+ if (!this.isDirty()) {
-+ return false;
-+ }
-+
-+ synchronized (this) {
-+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
-+ this.storageVisible = null;
-+ } else {
-+ if (this.storageVisible == null) {
-+ this.storageVisible = this.storageUpdating.clone();
-+ } else {
-+ if (this.storageUpdating != this.storageVisible) {
-+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
-+ }
-+ }
-+
-+ if (this.storageUpdating != this.storageVisible) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = this.storageVisible;
-+ }
-+ this.updatingDirty = false;
-+ this.stateVisible = this.stateUpdating;
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: visible
-+ public DataLayer toVanillaNibble() {
-+ synchronized (this) {
-+ switch (this.stateVisible) {
-+ case INIT_STATE_HIDDEN:
-+ case INIT_STATE_NULL:
-+ return null;
-+ case INIT_STATE_UNINIT:
-+ return new DataLayer();
-+ case INIT_STATE_INIT:
-+ return new DataLayer(this.storageVisible.clone());
-+ default:
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ /* x | (z << 4) | (y << 8) */
-+
-+ // operation type: updating
-+ public int getUpdating(final int x, final int y, final int z) {
-+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: updating
-+ public int getUpdating(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] bytes = this.storageUpdating;
-+ if (bytes == null) {
-+ return 0;
-+ }
-+ final byte value = bytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int x, final int y, final int z) {
-+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] visibleBytes = this.storageVisible;
-+ if (visibleBytes == null) {
-+ return 0;
-+ }
-+ final byte value = visibleBytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int x, final int y, final int z, final int value) {
-+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int index, final int value) {
-+ if (!this.updatingDirty) {
-+ this.swapUpdatingAndMarkDirty();
-+ }
-+ final int shift = (index & 1) << 2;
-+ final int i = index >>> 1;
-+
-+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
-+ }
-+
-+ public static final class SaveState {
-+
-+ public final byte[] data;
-+ public final int state;
-+
-+ public SaveState(final byte[] data, final int state) {
-+ this.data = data;
-+ this.state = state;
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..43a2cce467d29f81ba57d77c03608e57857dd579
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java
-@@ -0,0 +1,709 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.Set;
-+
-+public final class SkyStarLightEngine extends StarLightEngine {
-+
-+ /*
-+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
-+
-+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
-+
-+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
-+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
-+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
-+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
-+ How do we solve this?
-+
-+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
-+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
-+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
-+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
-+ to see if any of its nibbles need to be de-initialised.
-+
-+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
-+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
-+
-+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
-+ around those.
-+ */
-+
-+ protected final int[] heightMapBlockChange = new int[16 * 16];
-+ {
-+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
-+ }
-+
-+ protected final boolean[] nullPropagationCheckCache;
-+
-+ public SkyStarLightEngine(final Level world) {
-+ super(true, world);
-+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
-+ }
-+ }
-+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ nibble.setNull();
-+ }
-+ }
-+
-+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
-+ if (!currNibble.isNullNibbleUpdating()) {
-+ // already initialised
-+ return;
-+ }
-+
-+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = this.minLightSection - 1;
-+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
-+ if (emptinessMap == null) {
-+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
-+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
-+ if (current == null || current.hasOnlyAir()) {
-+ continue;
-+ }
-+ } else {
-+ if (emptinessMap[currY - this.minSection]) {
-+ continue;
-+ }
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (chunkY > lowestY) {
-+ // we need to set this one to full
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ nibble.setNonNull();
-+ nibble.setFull();
-+ return;
-+ }
-+
-+ if (extrude) {
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ currNibble.setNonNull();
-+ currNibble.extrudeLower(nibble);
-+ break;
-+ }
-+ }
-+ } else {
-+ currNibble.setNonNull();
-+ }
-+ }
-+
-+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (nibble != null && nibble.isNullNibbleUpdating()) {
-+ // stop propagation in these areas
-+ this.nibbleCache[index] = null;
-+ nibble.updateVisible();
-+ }
-+ }
-+ }
-+
-+ // rets whether neighbours were init'd
-+
-+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final boolean extrudeInitialised) {
-+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
-+ // non-null. Propagation to these neighbours is necessary.
-+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
-+ // this nibble would be initialised). So, we don't have to initialise
-+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
-+ // to the neighbours on this horizontal plane are blocked.
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
-+ return false;
-+ }
-+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
-+
-+ // check horizontal neighbours
-+ boolean needInitNeighbours = false;
-+ neighbour_search:
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ needInitNeighbours = true;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+
-+ if (needInitNeighbours) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
-+ }
-+ }
-+ }
-+
-+ return needInitNeighbours;
-+ }
-+
-+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
-+ final int chunkX = worldX >> 4;
-+ int chunkY = worldY >> 4;
-+ final int chunkZ = worldZ >> 4;
-+
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, worldY, worldZ);
-+ }
-+
-+ for (;;) {
-+ if (++chunkY > this.maxLightSection) {
-+ return 15;
-+ }
-+
-+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, 0, worldZ);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setSkyEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getSkyNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setSkyNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ // can only use chunks for sky stuff if their sections have been init'd
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
-+ final int toSection) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (int y = toSection; y >= fromSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ final int y = (int)iterator.nextShort();
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, sections);
-+ }
-+
-+ @Override
-+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change direction of propagation
-+
-+ // same logic applies from BlockStarLightEngine#checkBlock
-+
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+
-+ if (currentLevel == 15) {
-+ // must re-propagate clobbered source
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
-+ );
-+ } else {
-+ this.setLightLevel(worldX, worldY, worldZ, 0);
-+ }
-+
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ );
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ if (expect == 15) {
-+ return expect;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ final BlockState conditionallyOpaqueState;
-+ if (opacity < 0) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else {
-+ conditionallyOpaqueState = null;
-+ opacity = Math.max(1, opacity);
-+ }
-+
-+ int level = 0;
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ this.rewriteNibbleCacheForSkylight(atChunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final int chunkX = atChunk.getPos().x;
-+ final int chunkZ = atChunk.getPos().z;
-+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
-+
-+ // setup heightmap for changes
-+ for (final BlockPos pos : positions) {
-+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
-+ final int curr = this.heightMapBlockChange[index];
-+ if (pos.getY() > curr) {
-+ this.heightMapBlockChange[index] = pos.getY();
-+ }
-+ }
-+
-+ // note: light sets are delayed while processing skylight source changes due to how
-+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
-+ // below nibbles are initialised they aren't reading from partially modified nibbles
-+
-+ // now we can recalculate the sources for the changed columns
-+ for (int index = 0; index < (16 * 16); ++index) {
-+ final int maxY = this.heightMapBlockChange[index];
-+ if (maxY == Integer.MIN_VALUE) {
-+ // not changed
-+ continue;
-+ }
-+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
-+
-+ final int columnX = (index & 15) | (chunkX << 4);
-+ final int columnZ = (index >>> 4) | (chunkZ << 4);
-+
-+ // try and propagate from the above y
-+ // delay light set until after processing all sources to setup
-+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
-+
-+ // maxPropagationY is now the highest block that could not be propagated to
-+
-+ // remove all sources below that are 15
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
-+
-+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
-+ if ((currY & 15) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
-+ }
-+
-+ // ensure section below is always checked
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
-+ if (nibble == null) {
-+ // advance currY to the the top of the section below
-+ currY = (currY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+ continue;
-+ }
-+
-+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
-+ break;
-+ }
-+
-+ // delay light set until after processing all sources to setup
-+ this.appendToDecreaseQueue(
-+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // do not set transparent blocks for the same reason we don't in the checkBlock method
-+ );
-+ }
-+ }
-+ }
-+
-+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
-+ // immediate light value
-+ this.processDelayedIncreases();
-+ this.processDelayedDecreases();
-+
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected final int[] heightMapGen = new int[32 * 32];
-+
-+ @Override
-+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ int highestNonEmptySection = this.maxSection;
-+ while (highestNonEmptySection == (this.minSection - 1) ||
-+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
-+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
-+ // try propagate FULL to neighbours
-+
-+ // check neighbours to see if we need to propagate into them
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourX = chunkX + direction.x;
-+ final int neighbourZ = chunkZ + direction.z;
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
-+ if (neighbourNibble == null) {
-+ // unloaded neighbour
-+ // most of the time we fall here
-+ continue;
-+ }
-+
-+ // it looks like we need to propagate into the neighbour
-+
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (direction.x != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (direction.z < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
-+
-+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
-+ );
-+ }
-+ }
-+ }
-+
-+ if (highestNonEmptySection-- == (this.minSection - 1)) {
-+ break;
-+ }
-+ }
-+
-+ if (highestNonEmptySection >= this.minSection) {
-+ // fill out our other sources
-+ final int minX = chunkPos.x << 4;
-+ final int maxX = chunkPos.x << 4 | 15;
-+ final int minZ = chunkPos.z << 4;
-+ final int maxZ = chunkPos.z << 4 | 15;
-+ final int startY = highestNonEmptySection << 4 | 15;
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
-+ }
-+ }
-+ } // else: apparently the chunk is empty
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ // no need to rewrite the nibble cache again
-+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+ } else {
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+
-+ protected final void processDelayedIncreases() {
-+ // copied from performLightIncrease
-+ final long[] queue = this.increaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+ }
-+
-+ protected final void processDelayedDecreases() {
-+ // copied from performLightDecrease
-+ final long[] queue = this.decreaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+
-+ this.setLightLevel(posX, posY, posZ, 0);
-+ }
-+ }
-+
-+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
-+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
-+ // clobbering the light values will result in broken propagation)
-+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
-+ final boolean extrudeInitialised, final boolean delayLightSet) {
-+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
-+
-+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
-+ return startY;
-+ }
-+
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+
-+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
-+
-+ for (;startY >= (this.minLightSection << 4); --startY) {
-+ if ((startY & 15) == 15) {
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+ }
-+ final BlockState current = this.getBlockState(worldX, startY, worldZ);
-+
-+ final VoxelShape fromShape;
-+ if (above.isConditionallyFullOpaque()) {
-+ this.mutablePos2.set(worldX, startY + 1, worldZ);
-+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ // above wont let us propagate
-+ break;
-+ }
-+ } else {
-+ fromShape = Shapes.empty();
-+ }
-+
-+ final int opacityIfCached = current.getOpacityIfCached();
-+ // does light propagate from the top down?
-+ if (opacityIfCached != -1) {
-+ if (opacityIfCached != 0) {
-+ // we cannot propagate 15 through this
-+ break;
-+ }
-+ // most of the time it falls here.
-+ // add to propagate
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ );
-+ } else {
-+ mutablePos.set(worldX, startY, worldZ);
-+ long flags = 0L;
-+ if (current.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ // can't propagate here, we're done on this column.
-+ break;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = current.getLightBlock(world, mutablePos);
-+ if (opacity > 0) {
-+ // let the queued value (if any) handle it from here.
-+ break;
-+ }
-+
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | flags
-+ );
-+ }
-+
-+ above = current;
-+
-+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
-+ // we skip empty sections here, as this is just an easy way of making sure the above block
-+ // can propagate through air.
-+
-+ // nothing can propagate in null sections, remove the queue entry for it
-+ --this.increaseQueueInitialLength;
-+
-+ // advance currY to the the top of the section below
-+ startY = (startY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+
-+ // make sure this is marked as AIR
-+ above = AIR_BLOCK_STATE;
-+ } else if (!delayLightSet) {
-+ this.setLightLevel(worldX, startY, worldZ, 15);
-+ }
-+ }
-+
-+ return startY;
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ad1eeebe6de219143492b94da309cb54ae9e0a5b
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java
-@@ -0,0 +1,1572 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.CoordinateUtils;
-+import ca.spottedleaf.starlight.common.util.IntegerUtil;
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.LevelHeightAccessor;
-+import net.minecraft.world.level.LightLayer;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public abstract class StarLightEngine {
-+
-+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
-+
-+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
-+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
-+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
-+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
-+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
-+ };
-+
-+ protected static enum AxisDirection {
-+
-+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
-+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
-+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
-+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
-+
-+ static {
-+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
-+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
-+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
-+ }
-+
-+ protected AxisDirection opposite;
-+
-+ public final int x;
-+ public final int y;
-+ public final int z;
-+ public final Direction nms;
-+ public final long everythingButThisDirection;
-+ public final long everythingButTheOppositeDirection;
-+
-+ AxisDirection(final int x, final int y, final int z) {
-+ this.x = x;
-+ this.y = y;
-+ this.z = z;
-+ this.nms = Direction.fromDelta(x, y, z);
-+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
-+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
-+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
-+ }
-+
-+ public AxisDirection getOpposite() {
-+ return this.opposite;
-+ }
-+ }
-+
-+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
-+ // for explaining how light propagates via breadth-first search
-+
-+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
-+ // exactly how the vanilla light engine should behave for minecraft.
-+
-+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
-+ protected final LevelChunkSection[] sectionCache;
-+
-+ // the exact same as above, except for storing fast access to SWMRNibbleArray
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final SWMRNibbleArray[] nibbleCache;
-+
-+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final boolean[] notifyUpdateCache;
-+
-+ // always initialsed during start of lighting.
-+ // index = x + (z * 5)
-+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
-+
-+ // index = x + (z * 5)
-+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
-+
-+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
-+
-+ protected int encodeOffsetX;
-+ protected int encodeOffsetY;
-+ protected int encodeOffsetZ;
-+
-+ protected int coordinateOffset;
-+
-+ protected int chunkOffsetX;
-+ protected int chunkOffsetY;
-+ protected int chunkOffsetZ;
-+
-+ protected int chunkIndexOffset;
-+ protected int chunkSectionIndexOffset;
-+
-+ protected final boolean skylightPropagator;
-+ protected final int emittedLightMask;
-+ protected final boolean isClientSide;
-+
-+ protected final Level world;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+ protected final int minSection;
-+ protected final int maxSection;
-+
-+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
-+ this.skylightPropagator = skylightPropagator;
-+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
-+ this.isClientSide = world.isClientSide;
-+ this.world = world;
-+ this.minLightSection = WorldUtil.getMinLightSection(world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+
-+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ }
-+
-+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
-+ // 31 = center + encodeOffset
-+ this.encodeOffsetX = 31 - centerX;
-+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
-+ this.encodeOffsetZ = 31 - centerZ;
-+
-+ // coordinateIndex = x | (z << 6) | (y << 12)
-+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
-+
-+ // 2 = (centerX >> 4) + chunkOffset
-+ this.chunkOffsetX = 2 - (centerX >> 4);
-+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
-+ this.chunkOffsetZ = 2 - (centerZ >> 4);
-+
-+ // chunk index = x + (5 * z)
-+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
-+
-+ // chunk section index = x + (5 * z) + ((5*5) * y)
-+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
-+ }
-+
-+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
-+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
-+ final int centerChunkX = centerX >> 4;
-+ final int centerChunkY = centerY >> 4;
-+ final int centerChunkZ = centerZ >> 4;
-+
-+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
-+
-+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
-+
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ final int cx = centerChunkX + dx;
-+ final int cz = centerChunkZ + dz;
-+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
-+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
-+
-+ if (chunk == null) {
-+ if (relaxed | isTwoRadius) {
-+ continue;
-+ }
-+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
-+ }
-+
-+ if (!this.canUseChunk(chunk)) {
-+ continue;
-+ }
-+
-+ this.setChunkInCache(cx, cz, chunk);
-+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
-+ if (!isTwoRadius) {
-+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
-+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
-+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
-+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
-+ }
-+
-+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
-+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
-+ }
-+
-+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
-+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
-+ }
-+ }
-+
-+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
-+
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
-+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
-+ }
-+
-+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
-+ }
-+ }
-+
-+ protected final void updateVisible(final LightChunkGetter lightAccess) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
-+ continue;
-+ }
-+
-+ final int chunkX = (index % 5) - this.chunkOffsetX;
-+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
-+ final int ySections = (this.maxSection - this.minSection) + 1;
-+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
-+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
-+ }
-+ }
-+ }
-+
-+ protected final void destroyCaches() {
-+ Arrays.fill(this.sectionCache, null);
-+ Arrays.fill(this.nibbleCache, null);
-+ Arrays.fill(this.chunkCache, null);
-+ Arrays.fill(this.emptinessMapCache, null);
-+ if (this.isClientSide) {
-+ Arrays.fill(this.notifyUpdateCache, false);
-+ }
-+ }
-+
-+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
-+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
-+ final LevelChunkSection section = this.sectionCache[sectionIndex];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
-+ }
-+
-+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
-+ }
-+
-+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
-+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set(localIndex, level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
-+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
-+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
-+ }
-+
-+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
-+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
-+ }
-+
-+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
-+
-+ for (int i = 0, len = ret.length; i < len; ++i) {
-+ ret[i] = new SWMRNibbleArray(null, true);
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
-+
-+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
-+
-+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
-+
-+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
-+
-+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
-+
-+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Set<BlockPos> positions, final Boolean[] changedSections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ if (changedSections != null) {
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ }
-+ if (!positions.isEmpty()) {
-+ this.propagateBlockChanges(lightAccess, chunk, positions);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
-+
-+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
-+
-+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
-+ // if ret == expect, then expect is the correct light value for pos
-+ // if ret < expect, then ret is the real light value
-+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect);
-+
-+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
-+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
-+
-+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (currNibble == null) {
-+ return;
-+ }
-+
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ chunkY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null) {
-+ continue;
-+ }
-+
-+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
-+ // both are zero, nothing to check.
-+ continue;
-+ }
-+
-+ // this chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ int centerDelayedChecks = 0;
-+ int neighbourDelayedChecks = 0;
-+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int neighbourX = currX + neighbourOffX;
-+ final int neighbourZ = currZ + neighbourOffZ;
-+
-+ final int currentIndex = (currX & 15) |
-+ ((currZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int currentLevel = currNibble.getUpdating(currentIndex);
-+
-+ final int neighbourIndex =
-+ (neighbourX & 15) |
-+ ((neighbourZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
-+
-+ // the checks are delayed because the checkBlock method clobbers light values - which then
-+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
-+ // way, they do have a negative performance impact due to simply queueing more values
-+
-+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
-+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
-+ }
-+
-+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
-+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
-+ }
-+ }
-+ }
-+
-+ final int currentChunkOffX = chunkX << 4;
-+ final int currentChunkOffZ = chunkZ << 4;
-+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
-+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
-+ final int chunkOffY = chunkY << 4;
-+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
-+ // try to queue neighbouring data together
-+ // index = x | (z << 4) | (y << 8)
-+ if (i < centerDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
-+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ currentChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ if (i < neighbourDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
-+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
-+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
-+ // This does not resolve skylight source problems.
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
-+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
-+ if (currNibble == null) {
-+ continue;
-+ }
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ currSectionY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
-+ // can't pull from 0
-+ continue;
-+ }
-+
-+ // neighbour chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = (chunkX << 4) - 1;
-+ } else {
-+ startX = (chunkX << 4) + 16;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = (chunkZ << 4) - 1;
-+ } else {
-+ startZ = (chunkZ << 4) + 16;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int level = neighbourNibble.getUpdating(
-+ (currX & 15)
-+ | ((currZ & 15) << 4)
-+ | ((currY & 15) << 8)
-+ );
-+
-+ if (level <= 1) {
-+ // nothing to propagate
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((level & 0xFL) << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
-+ );
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ final Boolean[] ret = new Boolean[sections.length];
-+
-+ for (int i = 0; i < sections.length; ++i) {
-+ if (sections[i] == null || sections[i].hasOnlyAir()) {
-+ ret[i] = Boolean.TRUE;
-+ } else {
-+ ret[i] = Boolean.FALSE;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Boolean[] emptinessChanges) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
-+
-+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // subclasses are guaranteed that this is always called before a changed block set
-+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
-+ // rets non-null when the emptiness map changed and needs to be updated
-+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final Boolean[] emptinessChanges, final boolean unlit) {
-+ final Level world = (Level)lightAccess.getLevel();
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+
-+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+ boolean[] ret = null;
-+ final boolean needsInit = unlit || chunkEmptinessMap == null;
-+ if (needsInit) {
-+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
-+ }
-+
-+ // update emptiness map
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ if (valueBoxed == null) {
-+ if (!needsInit) {
-+ continue;
-+ }
-+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
-+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
-+ }
-+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
-+ }
-+
-+ // now init neighbour nibbles
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ final int sectionY = sectionIndex + this.minSection;
-+ if (valueBoxed == null) {
-+ continue;
-+ }
-+
-+ final boolean empty = valueBoxed.booleanValue();
-+
-+ if (empty) {
-+ continue;
-+ }
-+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // if we're not empty, we also need to initialise nibbles
-+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ for (int dy = 1; dy >= -1; --dy) {
-+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ // check for de-init and lazy-init
-+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
-+ // init checks.
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // does this neighbour have 1 radius loaded?
-+ boolean neighboursLoaded = true;
-+ neighbour_loaded_search:
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
-+ neighboursLoaded = false;
-+ break neighbour_loaded_search;
-+ }
-+ }
-+ }
-+
-+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
-+ // check neighbours to see if we need to de-init this one
-+ boolean allEmpty = true;
-+ neighbour_search:
-+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int y = sectionY + dy2;
-+ if (y < this.minSection || y > this.maxSection) {
-+ // empty
-+ continue;
-+ }
-+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
-+ if (emptinessMap != null) {
-+ if (!emptinessMap[y - this.minSection]) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ } else {
-+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
-+ if (section != null && !section.hasOnlyAir()) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ if (allEmpty & neighboursLoaded) {
-+ // can only de-init when neighbours are loaded
-+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
-+ // to be correct
-+
-+ // all were empty, so de-init
-+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
-+ } else if (!allEmpty) {
-+ // must init
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, sections);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
-+ // chunks light values with respect to neighbours
-+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
-+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
-+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
-+
-+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+
-+ try {
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.lightChunk(lightAccess, chunk, true);
-+ this.setNibbles(chunk, nibbles);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
-+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
-+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
-+ // the region of chunks to relight
-+ // it's required that tickets are added for each chunk to keep them loaded
-+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
-+
-+ final int[] neighbourLightOrder = new int[] {
-+ // d = 0
-+ 0, 0,
-+ // d = 1
-+ -1, 0,
-+ 0, -1,
-+ 1, 0,
-+ 0, 1,
-+ // d = 2
-+ -1, 1,
-+ 1, 1,
-+ -1, -1,
-+ 1, -1,
-+ };
-+
-+ int lightCalls = 0;
-+
-+ for (final ChunkPos chunkPos : chunks) {
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
-+ if (chunk == null || !this.canUseChunk(chunk)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
-+ final int dx = neighbourLightOrder[i];
-+ final int dz = neighbourLightOrder[i + 1];
-+ final int neighbourX = dx + chunkX;
-+ final int neighbourZ = dz + chunkZ;
-+
-+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
-+ if (neighbour == null || !this.canUseChunk(neighbour)) {
-+ continue;
-+ }
-+
-+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
-+ // lit already called for neighbour, no need to light it now
-+ continue;
-+ }
-+
-+ // light neighbour chunk
-+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
-+ try {
-+ // insert all neighbouring chunks for this neighbour that we have data for
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int neighbourX2 = neighbourX + dx2;
-+ final int neighbourZ2 = neighbourZ + dz2;
-+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
-+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
-+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
-+ if (nibbles == null) {
-+ // we haven't lit this chunk
-+ continue;
-+ }
-+
-+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
-+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
-+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
-+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
-+ }
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ // now insert the neighbour chunk and light it
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
-+ nibblesByChunk.put(key, nibbles);
-+
-+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
-+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
-+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
-+
-+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
-+ emptinessMapByChunk.put(key, neighbourEmptiness);
-+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
-+ this.setEmptinessMap(neighbour, neighbourEmptiness);
-+ }
-+
-+ this.lightChunk(lightAccess, neighbour, false);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // done lighting all neighbours, so the chunk is now fully lit
-+
-+ // make sure nibbles are fully updated before calling back
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ for (final SWMRNibbleArray nibble : nibbles) {
-+ nibble.updateVisible();
-+ }
-+
-+ this.setNibbles(chunk, nibbles);
-+
-+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX));
-+ }
-+
-+ // now do callback
-+ if (chunkLightCallback != null) {
-+ chunkLightCallback.accept(chunkPos);
-+ }
-+ ++lightCalls;
-+ }
-+
-+ if (onComplete != null) {
-+ onComplete.accept(lightCalls);
-+ }
-+ }
-+
-+ // contains:
-+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
-+ // next 4 bits: propagated light level (0, 15]
-+ // next 6 bits: propagation direction bitset
-+ // next 24 bits: unused
-+ // last 3 bits: state flags
-+ // state flags:
-+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
-+ // updates for block sources
-+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
-+ // whether the propagation needs to check if its current level is equal to the expected level
-+ // used only in increase propagation
-+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
-+ // whether the propagation needs to consider if its block is conditionally transparent
-+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
-+
-+ protected long[] increaseQueue = new long[16 * 16 * 16];
-+ protected int increaseQueueInitialLength;
-+ protected long[] decreaseQueue = new long[16 * 16 * 16];
-+ protected int decreaseQueueInitialLength;
-+
-+ protected final long[] resizeIncreaseQueue() {
-+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
-+ }
-+
-+ protected final long[] resizeDecreaseQueue() {
-+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
-+ }
-+
-+ protected final void appendToIncreaseQueue(final long value) {
-+ final int idx = this.increaseQueueInitialLength++;
-+ long[] queue = this.increaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected final void appendToDecreaseQueue(final long value) {
-+ final int idx = this.decreaseQueueInitialLength++;
-+ long[] queue = this.decreaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
-+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
-+ static {
-+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
-+ final List<AxisDirection> directions = new ArrayList<>();
-+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
-+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
-+ }
-+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
-+ }
-+ }
-+
-+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.increaseQueueInitialLength;
-+ this.increaseQueueInitialLength = 0;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
-+
-+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
-+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
-+ // not at the level we expect, so something changed.
-+ continue;
-+ }
-+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
-+ // these are used to restore block sources after a propagation decrease
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want or unloaded
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.decreaseQueue;
-+ long[] increaseQueue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.decreaseQueueInitialLength;
-+ this.decreaseQueueInitialLength = 0;
-+ int increaseQueueLength = this.increaseQueueInitialLength;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final VoxelShape fromShape = (fromBlock.isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ // propagate sources we clobbered
-+ this.increaseQueueInitialLength = increaseQueueLength;
-+ this.performLightIncrease(lightAccess);
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e0338db4d6fa359029ed5edeacc3646aa98701f5
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-@@ -0,0 +1,674 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.CoordinateUtils;
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.DataLayer;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import net.minecraft.world.level.lighting.LevelLightEngine;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public final class StarLightInterface {
-+
-+ public static final TicketType<ChunkPos> CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong()));
-+
-+ /**
-+ * Can be {@code null}, indicating the light is all empty.
-+ */
-+ protected final Level world;
-+ protected final LightChunkGetter lightAccess;
-+
-+ protected final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
-+ protected final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
-+
-+ protected final LightQueue lightQueue = new LightQueue(this);
-+
-+ protected final LayerLightEventListener skyReader;
-+ protected final LayerLightEventListener blockReader;
-+ protected final boolean isClientSide;
-+
-+ protected final int minSection;
-+ protected final int maxSection;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+
-+ public final LevelLightEngine lightEngine;
-+
-+ private final boolean hasBlockLight;
-+ private final boolean hasSkyLight;
-+
-+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
-+ this.lightAccess = lightAccess;
-+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
-+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.isClientSide = !(this.world instanceof ServerLevel);
-+ if (this.world == null) {
-+ this.minSection = -4;
-+ this.maxSection = 19;
-+ this.minLightSection = -5;
-+ this.maxLightSection = 20;
-+ } else {
-+ this.minSection = WorldUtil.getMinSection(this.world);
-+ this.maxSection = WorldUtil.getMaxSection(this.world);
-+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
-+ }
-+ this.lightEngine = lightEngine;
-+ this.hasBlockLight = hasBlockLight;
-+ this.hasSkyLight = hasSkyLight;
-+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return null;
-+ }
-+
-+ final int sectionY = pos.getY();
-+
-+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
-+ return null;
-+ }
-+
-+ if (chunk.getSkyEmptinessMap() == null) {
-+ return null;
-+ }
-+
-+ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+
-+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
-+ return null;
-+ }
-+
-+ return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ }
-+
-+ public boolean hasSkyLight() {
-+ return this.hasSkyLight;
-+ }
-+
-+ public boolean hasBlockLight() {
-+ return this.hasBlockLight;
-+ }
-+
-+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasSkyLight) {
-+ return 0;
-+ }
-+ final int x = blockPos.getX();
-+ int y = blockPos.getY();
-+ final int z = blockPos.getZ();
-+
-+ final int minSection = this.minSection;
-+ final int maxSection = this.maxSection;
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return 15;
-+ }
-+
-+ int sectionY = y >> 4;
-+
-+ if (sectionY > maxLightSection) {
-+ return 15;
-+ }
-+
-+ if (sectionY < minLightSection) {
-+ sectionY = minLightSection;
-+ y = sectionY << 4;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles();
-+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
-+
-+ if (!immediate.isNullNibbleVisible()) {
-+ return immediate.getVisible(x, y, z);
-+ }
-+
-+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap();
-+
-+ if (emptinessMap == null) {
-+ return 15;
-+ }
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = minLightSection - 1;
-+ for (int currY = maxSection; currY >= minSection; --currY) {
-+ if (emptinessMap[currY - minSection]) {
-+ continue;
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (sectionY > lowestY) {
-+ return 15;
-+ }
-+
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
-+ if (!nibble.isNullNibbleVisible()) {
-+ return nibble.getVisible(x, 0, z);
-+ }
-+ }
-+
-+ // should never reach here
-+ return 15;
-+ }
-+
-+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasBlockLight) {
-+ return 0;
-+ }
-+ final int y = blockPos.getY();
-+ final int cy = y >> 4;
-+
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (cy < minLightSection || cy > maxLightSection) {
-+ return 0;
-+ }
-+
-+ if (chunk == null) {
-+ return 0;
-+ }
-+
-+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - minLightSection];
-+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
-+ }
-+
-+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
-+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
-+
-+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
-+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
-+ if (sky == 15) return 15;
-+ final int block = this.getBlockLightValue(pos, chunk);
-+ return Math.max(sky, block);
-+ }
-+
-+ public LayerLightEventListener getSkyReader() {
-+ return this.skyReader;
-+ }
-+
-+ public LayerLightEventListener getBlockReader() {
-+ return this.blockReader;
-+ }
-+
-+ public boolean isClientSide() {
-+ return this.isClientSide;
-+ }
-+
-+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
-+ if (this.world == null) {
-+ // empty world
-+ return null;
-+ }
-+
-+ final ServerChunkCache chunkProvider = ((ServerLevel)this.world).getChunkSource();
-+ final LevelChunk fullLoaded = chunkProvider.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
-+ if (fullLoaded != null) {
-+ return fullLoaded;
-+ }
-+
-+ return chunkProvider.getChunkAtImmediately(chunkX, chunkZ);
-+ }
-+
-+ public boolean hasUpdates() {
-+ return !this.lightQueue.isEmpty();
-+ }
-+
-+ public Level getWorld() {
-+ return this.world;
-+ }
-+
-+ public LightChunkGetter getLightAccess() {
-+ return this.lightAccess;
-+ }
-+
-+ protected final SkyStarLightEngine getSkyLightEngine() {
-+ if (this.cachedSkyPropagators == null) {
-+ return null;
-+ }
-+ final SkyStarLightEngine ret;
-+ synchronized (this.cachedSkyPropagators) {
-+ ret = this.cachedSkyPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new SkyStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) {
-+ if (this.cachedSkyPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedSkyPropagators) {
-+ this.cachedSkyPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ protected final BlockStarLightEngine getBlockLightEngine() {
-+ if (this.cachedBlockPropagators == null) {
-+ return null;
-+ }
-+ final BlockStarLightEngine ret;
-+ synchronized (this.cachedBlockPropagators) {
-+ ret = this.cachedBlockPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new BlockStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) {
-+ if (this.cachedBlockPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedBlockPropagators) {
-+ this.cachedBlockPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
-+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueBlockChange(pos);
-+ }
-+
-+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ if (this.world == null) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
-+ }
-+
-+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
-+ final IntConsumer onComplete) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
-+ blockEngine == null ? onComplete : null);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
-+ this.checkSkyEdges(chunkX, chunkZ);
-+ this.checkBlockEdges(chunkX, chunkZ);
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void scheduleChunkLight(final ChunkPos pos, final Runnable run) {
-+ this.lightQueue.queueChunkLighting(pos, run);
-+ }
-+
-+ public void removeChunkTasks(final ChunkPos pos) {
-+ this.lightQueue.removeChunk(pos);
-+ }
-+
-+ public void propagateChanges() {
-+ if (this.lightQueue.isEmpty()) {
-+ return;
-+ }
-+
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ LightQueue.ChunkTasks task;
-+ while ((task = this.lightQueue.removeFirstTask()) != null) {
-+ if (task.lightTasks != null) {
-+ for (final Runnable run : task.lightTasks) {
-+ run.run();
-+ }
-+ }
-+
-+ final long coordinate = task.chunkCoordinate;
-+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
-+
-+ final Set<BlockPos> positions = task.changedPositions;
-+ final Boolean[] sectionChanges = task.changedSectionSet;
-+
-+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+
-+ if (skyEngine != null && task.queuedEdgeChecksSky != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky);
-+ }
-+ if (blockEngine != null && task.queuedEdgeChecksBlock != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock);
-+ }
-+
-+ task.onComplete.complete(null);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public static final class LightQueue {
-+
-+ protected final Long2ObjectLinkedOpenHashMap<ChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
-+ protected final StarLightInterface manager;
-+
-+ public LightQueue(final StarLightInterface manager) {
-+ this.manager = manager;
-+ }
-+
-+ public synchronized boolean isEmpty() {
-+ return this.chunkTasks.isEmpty();
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ tasks.changedPositions.add(pos.immutable());
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ if (tasks.changedSectionSet == null) {
-+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1];
-+ }
-+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ if (tasks.lightTasks == null) {
-+ tasks.lightTasks = new ArrayList<>();
-+ }
-+ tasks.lightTasks.add(lightTask);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks;
-+ }
-+
-+ public void removeChunk(final ChunkPos pos) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos));
-+ }
-+ if (tasks != null) {
-+ tasks.onComplete.complete(null);
-+ }
-+ }
-+
-+ public synchronized ChunkTasks removeFirstTask() {
-+ if (this.chunkTasks.isEmpty()) {
-+ return null;
-+ }
-+ return this.chunkTasks.removeFirst();
-+ }
-+
-+ public static final class ChunkTasks {
-+
-+ public final Set<BlockPos> changedPositions = new ObjectOpenHashSet<>();
-+ public Boolean[] changedSectionSet;
-+ public ShortOpenHashSet queuedEdgeChecksSky;
-+ public ShortOpenHashSet queuedEdgeChecksBlock;
-+ public List<Runnable> lightTasks;
-+
-+ public boolean isTicketAdded = false;
-+ public final CompletableFuture<Void> onComplete = new CompletableFuture<>();
-+
-+ public final long chunkCoordinate;
-+
-+ public ChunkTasks(final long chunkCoordinate) {
-+ this.chunkCoordinate = chunkCoordinate;
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..16a4a14e7ccf9e4d7fdf1166674fe8f529c06d39
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java
-@@ -0,0 +1,128 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class CoordinateUtils {
-+
-+ // dx, dz are relative to the target chunk
-+ // dx, dz in [-radius, radius]
-+ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
-+ return (dx + radius) + (2 * radius + 1)*(dz + radius);
-+ }
-+
-+ // the chunk keys are compatible with vanilla
-+
-+ public static long getChunkKey(final BlockPos pos) {
-+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final Entity entity) {
-+ return ((long)(Mth.floor(entity.getZ()) >> 4) << 32) | ((Mth.floor(entity.getX()) >> 4) & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final ChunkPos pos) {
-+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final SectionPos pos) {
-+ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
-+
-+ public static int getChunkX(final long chunkKey) {
-+ return (int)chunkKey;
-+ }
-+
-+ public static int getChunkZ(final long chunkKey) {
-+ return (int)(chunkKey >>> 32);
-+ }
-+
-+ public static int getChunkCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate) >> 4;
-+ }
-+
-+ // the section keys are compatible with vanilla's
-+
-+ static final int SECTION_X_BITS = 22;
-+ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
-+ static final int SECTION_Y_BITS = 20;
-+ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
-+ static final int SECTION_Z_BITS = 22;
-+ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
-+ // format is y,z,x (in order of LSB to MSB)
-+ static final int SECTION_Y_SHIFT = 0;
-+ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
-+ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
-+ static final int SECTION_TO_BLOCK_SHIFT = 4;
-+
-+ public static long getChunkSectionKey(final int x, final int y, final int z) {
-+ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final SectionPos pos) {
-+ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
-+ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final BlockPos pos) {
-+ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+ }
-+
-+ public static long getChunkSectionKey(final Entity entity) {
-+ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+ }
-+
-+ public static int getChunkSectionX(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
-+ }
-+
-+ public static int getChunkSectionY(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
-+ }
-+
-+ public static int getChunkSectionZ(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
-+ }
-+
-+ // the block coordinates are not necessarily compatible with vanilla's
-+
-+ public static int getBlockCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate);
-+ }
-+
-+ public static long getBlockKey(final int x, final int y, final int z) {
-+ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
-+ }
-+
-+ public static long getBlockKey(final BlockPos pos) {
-+ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
-+ }
-+
-+ public static long getBlockKey(final Entity entity) {
-+ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
-+ }
-+
-+ private CoordinateUtils() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..fabf1e97c019c7365212f40018dcd08d3b828113
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java
-@@ -0,0 +1,242 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+public final class IntegerUtil {
-+
-+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
-+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
-+
-+ public static int ceilLog2(final int value) {
-+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static long ceilLog2(final long value) {
-+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final int value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final long value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int roundCeilLog2(final int value) {
-+ // optimized variant of 1 << (32 - leading(val - 1))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
-+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
-+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static long roundCeilLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static int roundFloorLog2(final int value) {
-+ // optimized variant of 1 << (31 - leading(val))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
-+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
-+ }
-+
-+ public static long roundFloorLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
-+ }
-+
-+ public static boolean isPowerOfTwo(final int n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static boolean isPowerOfTwo(final long n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static int getTrailingBit(final int n) {
-+ return -n & n;
-+ }
-+
-+ public static long getTrailingBit(final long n) {
-+ return -n & n;
-+ }
-+
-+ public static int trailingZeros(final int n) {
-+ return Integer.numberOfTrailingZeros(n);
-+ }
-+
-+ public static int trailingZeros(final long n) {
-+ return Long.numberOfTrailingZeros(n);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorMultiple(final long numbers) {
-+ return (int)(numbers >>> 32);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorShift(final long numbers) {
-+ return (int)numbers;
-+ }
-+
-+ // copied from hacker's delight (signed division magic value)
-+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
-+ public static long getDivisorNumbers(final int d) {
-+ final int ad = branchlessAbs(d);
-+
-+ if (ad < 2) {
-+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
-+ }
-+
-+ final int two31 = 0x80000000;
-+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
-+
-+ /*
-+ Signed usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int sign = number >> 31;
-+ int result = (int)(((long)number * mul) >>> magic) - sign;
-+ */
-+ /*
-+ Unsigned usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int result = (int)(((long)number * mul) >>> magic);
-+ */
-+
-+ int p = 31;
-+
-+ // all these variables are UNSIGNED!
-+ int t = two31 + (d >>> 31);
-+ int anc = t - 1 - (int)((t & mask)%ad);
-+ int q1 = (int)((two31 & mask)/(anc & mask));
-+ int r1 = two31 - q1*anc;
-+ int q2 = (int)((two31 & mask)/(ad & mask));
-+ int r2 = two31 - q2*ad;
-+ int delta;
-+
-+ do {
-+ p = p + 1;
-+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
-+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
-+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
-+ q1 = q1 + 1;
-+ r1 = r1 - anc;
-+ }
-+ q2 = 2*q2; // Update q2 = 2**p/|d|.
-+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
-+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
-+ q2 = q2 + 1;
-+ r2 = r2 - ad;
-+ }
-+ delta = ad - r2;
-+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
-+
-+ int magicNum = q2 + 1;
-+ if (d < 0) {
-+ magicNum = -magicNum;
-+ }
-+ int shift = p;
-+ return ((long)magicNum << 32) | shift;
-+ }
-+
-+ public static int branchlessAbs(final int val) {
-+ // -n = -1 ^ n + 1
-+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ public static long branchlessAbs(final long val) {
-+ // -n = -1 ^ n + 1
-+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ //https://github.com/skeeto/hash-prospector for hash functions
-+
-+ //score = ~590.47984224483832
-+ public static int hash0(int x) {
-+ x *= 0x36935555;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ //score = ~310.01596637036749
-+ public static int hash1(int x) {
-+ x ^= x >>> 15;
-+ x *= 0x356aaaad;
-+ x ^= x >>> 17;
-+ return x;
-+ }
-+
-+ public static int hash2(int x) {
-+ x ^= x >>> 16;
-+ x *= 0x7feb352d;
-+ x ^= x >>> 15;
-+ x *= 0x846ca68b;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ public static int hash3(int x) {
-+ x ^= x >>> 17;
-+ x *= 0xed5ad4bb;
-+ x ^= x >>> 11;
-+ x *= 0xac4c1b51;
-+ x ^= x >>> 15;
-+ x *= 0x31848bab;
-+ x ^= x >>> 14;
-+ return x;
-+ }
-+
-+ //score = ~365.79959673201887
-+ public static long hash1(long x) {
-+ x ^= x >>> 27;
-+ x *= 0xb24924b71d2d354bL;
-+ x ^= x >>> 28;
-+ return x;
-+ }
-+
-+ //h2 hash
-+ public static long hash2(long x) {
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ return x;
-+ }
-+
-+ public static long hash3(long x) {
-+ x ^= x >>> 45;
-+ x *= 0xc161abe5704b6c79L;
-+ x ^= x >>> 41;
-+ x *= 0xe3e5389aedbc90f7L;
-+ x ^= x >>> 56;
-+ x *= 0x1f9aba75a52db073L;
-+ x ^= x >>> 53;
-+ return x;
-+ }
-+
-+ private IntegerUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c2903150c8fc6955f4f4f71acc932b6c2ac83484
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java
-@@ -0,0 +1,192 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import ca.spottedleaf.starlight.common.light.SWMRNibbleArray;
-+import ca.spottedleaf.starlight.common.light.StarLightEngine;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import org.slf4j.Logger;
-+
-+public final class SaveUtil {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ private static final int STARLIGHT_LIGHT_VERSION = 9;
-+
-+ public static int getLightVersion() {
-+ return STARLIGHT_LIGHT_VERSION;
-+ }
-+
-+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
-+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
-+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
-+
-+ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
-+ try {
-+ saveLightHookReal(world, chunk, nbt);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
-+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
-+ }
-+ }
-+
-+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
-+ if (tag == null) {
-+ return;
-+ }
-+
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles();
-+ SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles();
-+
-+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
-+ // diff start - store our tag for whether light data is init'd
-+ if (lit) {
-+ tag.putBoolean("isLightOn", false);
-+ }
-+ // diff end - store our tag for whether light data is init'd
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+
-+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
-+
-+ ListTag sectionsStored = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sectionsStored.size(); ++i) {
-+ CompoundTag sectionStored = sectionsStored.getCompound(i);
-+ int k = sectionStored.getByte("Y");
-+
-+ // strip light data
-+ sectionStored.remove("BlockLight");
-+ sectionStored.remove("SkyLight");
-+
-+ if (!sectionStored.isEmpty()) {
-+ sections[k - minSection] = sectionStored;
-+ }
-+ }
-+
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
-+ for (int i = minSection; i <= maxSection; ++i) {
-+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
-+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
-+ if (blockNibble != null || skyNibble != null) {
-+ CompoundTag section = sections[i - minSection];
-+ if (section == null) {
-+ section = new CompoundTag();
-+ section.putByte("Y", (byte)i);
-+ sections[i - minSection] = section;
-+ }
-+
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+
-+ if (blockNibble != null) {
-+ if (blockNibble.data != null) {
-+ section.putByteArray("BlockLight", blockNibble.data);
-+ }
-+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
-+ }
-+
-+ if (skyNibble != null) {
-+ if (skyNibble.data != null) {
-+ section.putByteArray("SkyLight", skyNibble.data);
-+ }
-+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
-+ }
-+ }
-+ }
-+ }
-+
-+ // rewrite section list
-+ sectionsStored.clear();
-+ for (CompoundTag section : sections) {
-+ if (section != null) {
-+ sectionsStored.add(section);
-+ }
-+ }
-+ tag.put("sections", sectionsStored);
-+ if (lit) {
-+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
-+ }
-+ }
-+
-+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ try {
-+ loadLightHookReal(world, pos, tag, into);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
-+ // lighting in both cases.
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
-+ }
-+ }
-+
-+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ if (into == null) {
-+ return;
-+ }
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ into.setLightCorrect(false); // mark as unlit in case we fail parsing
-+
-+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
-+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
-+
-+
-+ // start copy from the original method
-+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
-+ boolean canReadSky = world.dimensionType().hasSkyLight();
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
-+ ListTag sections = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sections.size(); ++i) {
-+ CompoundTag sectionData = sections.getCompound(i);
-+ int y = sectionData.getByte("Y");
-+
-+ if (sectionData.contains("BlockLight", 7)) {
-+ // this is where our diff is
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
-+ }
-+
-+ if (canReadSky) {
-+ if (sectionData.contains("SkyLight", 7)) {
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
-+ }
-+ }
-+ }
-+ }
-+ // end copy from vanilla
-+
-+ into.setBlockNibbles(blockNibbles);
-+ into.setSkyNibbles(skyNibbles);
-+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
-+ }
-+
-+ private SaveUtil() {}
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..dd995e25ae620ae36cd5eecb2fe10ad034ba50d2
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java
-@@ -0,0 +1,47 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+ // min, max are inclusive
-+
-+ public static int getMaxSection(final LevelHeightAccessor world) {
-+ return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+ }
-+
-+ public static int getMinSection(final LevelHeightAccessor world) {
-+ return world.getMinSection();
-+ }
-+
-+ public static int getMaxLightSection(final LevelHeightAccessor world) {
-+ return getMaxSection(world) + 1;
-+ }
-+
-+ public static int getMinLightSection(final LevelHeightAccessor world) {
-+ return getMinSection(world) - 1;
-+ }
-+
-+
-+
-+ public static int getTotalSections(final LevelHeightAccessor world) {
-+ return getMaxSection(world) - getMinSection(world) + 1;
-+ }
-+
-+ public static int getTotalLightSections(final LevelHeightAccessor world) {
-+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+ }
-+
-+ public static int getMinBlockY(final LevelHeightAccessor world) {
-+ return getMinSection(world) << 4;
-+ }
-+
-+ public static int getMaxBlockY(final LevelHeightAccessor world) {
-+ return (getMaxSection(world) << 4) | 15;
-+ }
-+
-+ private WorldUtil() {
-+ throw new RuntimeException();
-+ }
-+
-+}
-diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
-index 46bf42d5ea9e7b046f962531c5962d287cf44a41..a3f43dccb796f30f6e9389e1ae182f06e9024e96 100644
---- a/src/main/java/io/papermc/paper/command/PaperCommand.java
-+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
-@@ -42,6 +42,7 @@ public final class PaperCommand extends Command {
- commands.put(Set.of("dumpitem"), new DumpItemCommand());
- commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
- commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
-+ commands.put(Set.of("fixlight"), new FixLightCommand());
-
- return commands.entrySet().stream()
- .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..56524cbe4303901007e1e7fb3703a19efbf79ae7
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
-@@ -0,0 +1,109 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import io.papermc.paper.command.PaperSubcommand;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ThreadedLevelLightEngine;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.entity.Player;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class FixLightCommand implements PaperSubcommand {
-+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ this.doFixLight(sender, args);
-+ return true;
-+ }
-+
-+ private void doFixLight(final CommandSender sender, final String[] args) {
-+ if (!(sender instanceof Player)) {
-+ sender.sendMessage(text("Only players can use this command", RED));
-+ return;
-+ }
-+ @Nullable Runnable post = null;
-+ int radius = 2;
-+ if (args.length > 0) {
-+ try {
-+ final int parsed = Integer.parseInt(args[0]);
-+ if (parsed < 0) {
-+ sender.sendMessage(text("Radius cannot be negative!", RED));
-+ return;
-+ }
-+ final int maxRadius = 32;
-+ radius = Math.min(maxRadius, parsed);
-+ if (radius != parsed) {
-+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED));
-+ }
-+ } catch (final Exception e) {
-+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED));
-+ return;
-+ }
-+ }
-+
-+ CraftPlayer player = (CraftPlayer) sender;
-+ ServerPlayer handle = player.getHandle();
-+ ServerLevel world = (ServerLevel) handle.level();
-+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
-+ this.starlightFixLight(handle, world, lightengine, radius, post);
-+ }
-+
-+ private void starlightFixLight(
-+ final ServerPlayer sender,
-+ final ServerLevel world,
-+ final ThreadedLevelLightEngine lightengine,
-+ final int radius,
-+ final @Nullable Runnable done
-+ ) {
-+ final long start = System.nanoTime();
-+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
-+
-+ final int[] pending = new int[1];
-+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ ++pending[0];
-+ }
-+
-+ final int[] relitChunks = new int[1];
-+ lightengine.relight(chunks,
-+ (final ChunkPos chunkPos) -> {
-+ ++relitChunks[0];
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit chunk ", BLUE), text(chunkPos.toString()),
-+ text(", progress: ", BLUE), text((int) (Math.round(100.0 * (double) (relitChunks[0]) / (double) pending[0])) + "%")
-+ ));
-+ },
-+ (final int totalRelit) -> {
-+ final long end = System.nanoTime();
-+ final long diff = Math.round(1.0e-6 * (end - start));
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit ", BLUE), text(totalRelit),
-+ text(" chunks. Took ", BLUE), text(diff + "ms")
-+ ));
-+ if (done != null) {
-+ done.run();
-+ }
-+ }
-+ );
-+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
-+ }
-+}
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index b12921579cb9ab3cbf5607841cc84f2f843624ea..88729d92878f98729eb5669cce5ae5b1418865a1 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -51,7 +51,7 @@ public class ChunkHolder {
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
-- private CompletableFuture<ChunkAccess> chunkToSave;
-+ public CompletableFuture<ChunkAccess> chunkToSave; // Paper - public
- @Nullable
- private final DebugBuffer<ChunkHolder.ChunkSaveDebug> chunkToSaveHistory;
- public int oldTicketLevel;
-@@ -261,6 +261,12 @@ public class ChunkHolder {
- }
- }
-
-+ // Paper start - starlight
-+ public void broadcast(Packet<?> packet, boolean onChunkViewEdge) {
-+ this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet);
-+ }
-+ // Paper end - starlight
-+
- public void broadcastChanges(LevelChunk chunk) {
- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
- Level world = chunk.getLevel();
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 35f627c58e93c03ee58b44877398432bba57dc2d..d3f63185edd1db9fab3887ea3f08982435b3a23c 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -128,7 +128,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private final LongSet entitiesInLevel;
- public final ServerLevel level;
- private final ThreadedLevelLightEngine lightEngine;
-- private final BlockableEventLoop<Runnable> mainThreadExecutor;
-+ public final BlockableEventLoop<Runnable> mainThreadExecutor; // Paper - public
- public ChunkGenerator generator;
- private final RandomState randomState;
- private final ChunkGeneratorStructureState chunkGeneratorState;
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index c473cb1888e9ab0e91ba44f1439b81742758304e..7a48ae2ba962ff56d0abff581b51f28b48bd9aae 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -379,7 +379,7 @@ public abstract class DistanceManager {
- }
-
- public void removeTicketsOnClosing() {
-- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
-+ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-index 1dfae40ec19c4df0a97359941cf2c948cd1c9cb2..f206df06a7d8895175db31d4a840d7467ffe826f 100644
---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-@@ -23,6 +23,17 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
- import net.minecraft.world.level.lighting.LevelLightEngine;
- import org.slf4j.Logger;
-
-+// Paper start
-+import ca.spottedleaf.starlight.common.light.StarLightEngine;
-+import io.papermc.paper.util.CoordinateUtils;
-+import java.util.function.Supplier;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+// Paper end
-+
- public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
- public static final int DEFAULT_BATCH_SIZE = 1000;
- private static final Logger LOGGER = LogUtils.getLogger();
-@@ -33,6 +44,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- private final int taskPerBatch = 1000;
- private final AtomicBoolean scheduled = new AtomicBoolean();
-
-+ // Paper start - replace light engine impl
-+ protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine;
-+ public final boolean hasBlockLight;
-+ public final boolean hasSkyLight;
-+ // Paper end - replace light engine impl
-+
- public ThreadedLevelLightEngine(
- LightChunkGetter chunkProvider,
- ChunkMap chunkStorage,
-@@ -40,11 +57,153 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- ProcessorMailbox<Runnable> processor,
- ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor
- ) {
-- super(chunkProvider, true, hasBlockLight);
-+ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state
- this.chunkMap = chunkStorage;
- this.sorterMailbox = executor;
- this.taskMailbox = processor;
-+ // Paper start - replace light engine impl
-+ this.hasBlockLight = true;
-+ this.hasSkyLight = hasBlockLight; // Nice variable name.
-+ this.theLightEngine = new ca.spottedleaf.starlight.common.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this);
-+ // Paper end - replace light engine impl
-+ }
-+
-+ // Paper start - replace light engine impl
-+ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) {
-+ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ);
-+ }
-+
-+ protected long relightCounter;
-+
-+ public int relight(java.util.Set<ChunkPos> chunks_param,
-+ java.util.function.Consumer<ChunkPos> chunkLightCallback,
-+ java.util.function.IntConsumer onComplete) {
-+ if (!org.bukkit.Bukkit.isPrimaryThread()) {
-+ throw new IllegalStateException("Must only be called on the main thread");
-+ }
-+
-+ java.util.Set<ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks_param);
-+ // add tickets
-+ java.util.Map<ChunkPos, Long> ticketIds = new java.util.HashMap<>();
-+ int totalChunks = 0;
-+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final ChunkAccess chunk = (ChunkAccess)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ final Long id = Long.valueOf(this.relightCounter++);
-+
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
-+ ticketIds.put(chunkPos, id);
-+
-+ ++totalChunks;
-+ }
-+
-+ this.taskMailbox.tell(() -> {
-+ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
-+ chunkLightCallback.accept(chunkPos);
-+ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null), false);
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
-+ });
-+ }, onComplete);
-+ });
-+ this.tryScheduleUpdate();
-+
-+ return totalChunks;
-+ }
-+
-+ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
-+
-+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final Supplier<ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks> runnable) {
-+ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
-+
-+ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ);
-+ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
-+ // chunk scheduling, we could be lighting and generating a chunk at the same time
-+ return;
-+ }
-+
-+ if (center.getStatus() != ChunkStatus.FULL) {
-+ // do not keep chunk loaded, we are probably in a gen thread
-+ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen)
-+ runnable.get();
-+ return;
-+ }
-+
-+ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) {
-+ // ticket logic is not safe to run off-main, re-schedule
-+ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> {
-+ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable);
-+ });
-+ return;
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get();
-+
-+ if (updateFuture == null) {
-+ // not scheduled
-+ return;
-+ }
-+
-+ if (updateFuture.isTicketAdded) {
-+ // ticket already added
-+ return;
-+ }
-+ updateFuture.isTicketAdded = true;
-+
-+ final int references = this.chunksBeingWorkedOn.addTo(key, 1);
-+ if (references == 0) {
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
-+ }
-+
-+ updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> {
-+ final int newReferences = this.chunksBeingWorkedOn.get(key);
-+ if (newReferences == 1) {
-+ this.chunksBeingWorkedOn.remove(key);
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
-+ } else {
-+ this.chunksBeingWorkedOn.put(key, newReferences - 1);
-+ }
-+ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> {
-+ if (thr != null) {
-+ LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
-+ }
-+ });
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // route to new light engine
-+ return this.theLightEngine.hasUpdates();
-+ }
-+
-+ @Override
-+ public LayerLightEventListener getLayerListener(final LightLayer lightType) {
-+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader();
-+ }
-+
-+ @Override
-+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
-+ // need to use new light hooks for this
-+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness;
-+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
-+ if (sky == 15) return 15;
-+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos);
-+ return Math.max(sky, block);
- }
-+ // Paper end - replace light engine imp
-
- @Override
- public void close() {
-@@ -57,16 +216,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void checkBlock(BlockPos pos) {
-- BlockPos blockPos = pos.immutable();
-- this.addTask(
-- SectionPos.blockToSectionCoord(pos.getX()),
-- SectionPos.blockToSectionCoord(pos.getZ()),
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
-- );
-+ // Paper start - replace light engine impl
-+ final BlockPos posCopy = pos.immutable();
-+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
-+ return this.theLightEngine.blockChange(posCopy);
-+ });
-+ // Paper end - replace light engine impl
- }
-
- protected void updateChunkStatus(ChunkPos pos) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- super.retainData(pos, false);
- super.setLightEnabled(pos, false);
-@@ -84,17 +243,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void updateSectionStatus(SectionPos pos, boolean notReady) {
-- this.addTask(
-- pos.x(),
-- pos.z(),
-- () -> 0,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
-- );
-+ // Paper start - replace light engine impl
-+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
-+ return this.theLightEngine.sectionChange(pos, notReady);
-+ });
-+ // Paper end - replace light engine impl
- }
-
- @Override
- public void propagateLightSources(ChunkPos chunkPos) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- chunkPos.x,
- chunkPos.z,
-@@ -105,6 +263,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void setLightEnabled(ChunkPos pos, boolean retainData) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x,
- pos.z,
-@@ -115,6 +274,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x(),
- pos.z(),
-@@ -139,12 +299,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void retainData(ChunkPos pos, boolean retainData) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
- );
- }
-
- public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
-+ if (true) return CompletableFuture.completedFuture(chunk); // Paper - replace light engine impl
- ChunkPos chunkPos = chunk.getPos();
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- LevelChunkSection[] levelChunkSections = chunk.getSections();
-@@ -165,6 +327,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
-+ // Paper start - replace light engine impl
-+ if (true) {
-+ boolean lit = excludeBlocks;
-+ final ChunkPos chunkPos = chunk.getPos();
-+
-+ return CompletableFuture.supplyAsync(() -> {
-+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk);
-+ if (!lit) {
-+ chunk.setLightCorrect(false);
-+ this.theLightEngine.lightChunk(chunk, emptySections);
-+ chunk.setLightCorrect(true);
-+ } else {
-+ this.theLightEngine.forceLoadInChunk(chunk, emptySections);
-+ // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have
-+ // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should
-+ // catch what we miss here.
-+ this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z);
-+ }
-+
-+ this.chunkMap.releaseLightTicket(chunkPos);
-+ return chunk;
-+ }, (runnable) -> {
-+ this.theLightEngine.scheduleChunkLight(chunkPos, runnable);
-+ this.tryScheduleUpdate();
-+ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to light chunk " + chunkPos, throwable);
-+ }
-+ });
-+ }
-+ // Paper end - replace light engine impl
- ChunkPos chunkPos = chunk.getPos();
- chunk.setLightCorrect(false);
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-@@ -180,7 +373,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- public void tryScheduleUpdate() {
-- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
-+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine
- this.taskMailbox.tell(() -> {
- this.runUpdate();
- this.scheduled.set(false);
-@@ -201,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- objectListIterator.back(j);
-- super.runLightUpdates();
-+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
-
- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
-diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
-index 0d536d72ac918fbd403397ff369d10143ee9c204..6051e5f272838ef23276a90e21c2fc821ca155d1 100644
---- a/src/main/java/net/minecraft/server/level/TicketType.java
-+++ b/src/main/java/net/minecraft/server/level/TicketType.java
-@@ -26,6 +26,7 @@ public class TicketType<T> {
- public static final TicketType<ChunkPos> UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
- public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
- public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
-+ public static final TicketType<Long> CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting
-
- public static <T> TicketType<T> create(String name, Comparator<T> argumentComparator) {
- return new TicketType<>(name, argumentComparator, 0L);
-diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-index 386fbf79afe91af445f54aeab7d1296d1407a4d8..abbd4140cb4478a34a5185d8555f83d96c04d468 100644
---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-@@ -109,6 +109,27 @@ public class WorldGenRegion implements WorldGenLevel {
- }
- }
-
-+ // Paper start - starlight
-+ @Override
-+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
-+ }
-+
-+
-+ @Override
-+ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getRawBrightness(blockPos, subtract);
-+ }
-+ // Paper end - starlight
-+
- public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
- return this.level.getChunkSource().chunkMap.isOldChunkAround(chunkPos, checkRadius);
- }
-diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-index f87d9cb38caf3bf92fd32f2118f76799ede418db..c7da359c525522b55763e594a1db0c26a026b73f 100644
---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -812,6 +812,7 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles;
- this.instrument = blockbase_info.instrument;
- this.replaceable = blockbase_info.replaceable;
-+ this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper
- }
- // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
- private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
-@@ -848,6 +849,18 @@ public abstract class BlockBehaviour implements FeatureElement {
- return this.shapeExceedsCube;
- }
- // Paper end
-+ // Paper start - starlight
-+ protected int opacityIfCached = -1;
-+ // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15]
-+ public final int getOpacityIfCached() {
-+ return this.opacityIfCached;
-+ }
-+
-+ protected final boolean conditionallyFullOpaque;
-+ public final boolean isConditionallyFullOpaque() {
-+ return this.conditionallyFullOpaque;
-+ }
-+ // Paper end - starlight
-
- public void initCache() {
- this.fluidState = ((Block) this.owner).getFluidState(this.asState());
-@@ -856,6 +869,7 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
- }
- this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
-+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light
-
- this.legacySolid = this.calculateSolid();
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-index f4e3bd2ae4f63e6d3d25463a3635b8f89fecc068..1f8c72b6c7d8683d67880fa175843c73b3d39b78 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- @Nullable
- protected BlendingData blendingData;
- public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
-- protected ChunkSkyLightSources skyLightSources;
-+ // Paper - starlight - remove skyLightSources
- private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
- private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
- protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
-@@ -89,8 +89,55 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
- public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
- // CraftBukkit end
-+ // Paper start - rewrite light engine
-+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles;
-+
-+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles;
-+
-+ private volatile boolean[] skyEmptinessMap;
-+
-+ private volatile boolean[] blockEmptinessMap;
-+
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return this.blockNibbles;
-+ }
-+
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.blockNibbles = nibbles;
-+ }
-+
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return this.skyNibbles;
-+ }
-+
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.skyNibbles = nibbles;
-+ }
-+
-+ public boolean[] getSkyEmptinessMap() {
-+ return this.skyEmptinessMap;
-+ }
-+
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ this.skyEmptinessMap = emptinessMap;
-+ }
-+
-+ public boolean[] getBlockEmptinessMap() {
-+ return this.blockEmptinessMap;
-+ }
-+
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ this.blockEmptinessMap = emptinessMap;
-+ }
-+ // Paper end - rewrite light engine
-
- public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
-+ // Paper start - rewrite light engine
-+ if (!(this instanceof ImposterProtoChunk)) {
-+ this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ }
-+ // Paper end - rewrite light engine
- this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
- this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
- this.upgradeData = upgradeData;
-@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- this.inhabitedTime = inhabitedTime;
- this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
- this.blendingData = blendingData;
-- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
-+ // Paper - starlight - remove skyLightSources
- if (sectionArray != null) {
- if (this.sections.length == sectionArray.length) {
- System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
-@@ -510,12 +557,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- }
-
- public void initializeLightSources() {
-- this.skyLightSources.fillFrom(this);
-+ // Paper - starlight - remove skyLightSources
- }
-
- @Override
- public ChunkSkyLightSources getSkyLightSources() {
-- return this.skyLightSources;
-+ return null; // Paper - starlight - remove skyLightSources
- }
-
- public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-index 2ee1658532cb00d7bcd1d11e03f19d21ca7f2a9e..ac754827172a4de600d0a57a7d11853481a2dbf2 100644
---- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
- this.biome = biomeEntry;
- }
-
-+ // Paper start - starlight
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public boolean[] getSkyEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {}
-+
-+ @Override
-+ public boolean[] getBlockEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {}
-+ // Paper end - starlight
-+
- @Override
- public BlockState getBlockState(BlockPos pos) {
- return Blocks.VOID_AIR.defaultBlockState();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-index 2953e93965aa688be8fc1620580701ba0c9d907e..aa5dee839d4c0dbc3c2abee9b501ec250c575cb3 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk {
- this.allowWrites = propagateToWrapped;
- }
-
-+ // Paper start - rewrite light engine
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return this.wrapped.getBlockNibbles();
-+ }
-+
-+ @Override
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.wrapped.setBlockNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return this.wrapped.getSkyNibbles();
-+ }
-+
-+ @Override
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.wrapped.setSkyNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public boolean[] getSkyEmptinessMap() {
-+ return this.wrapped.getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ this.wrapped.setSkyEmptinessMap(emptinessMap);
-+ }
-+
-+ @Override
-+ public boolean[] getBlockEmptinessMap() {
-+ return this.wrapped.getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ this.wrapped.setBlockEmptinessMap(emptinessMap);
-+ }
-+ // Paper end - rewrite light engine
-+
- @Nullable
- @Override
- public BlockEntity getBlockEntity(BlockPos pos) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-index 8de6ad8b131061b2dae440dff71e2e6e7af2de39..bac191f92ea3735df19c68d5568c2c7962c8680f 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -222,6 +222,12 @@ public class LevelChunk extends ChunkAccess {
-
- public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
- this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
-+ // Paper start - rewrite light engine
-+ this.setBlockNibbles(protoChunk.getBlockNibbles());
-+ this.setSkyNibbles(protoChunk.getSkyNibbles());
-+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap());
-+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap());
-+ // Paper end - rewrite light engine
- Iterator iterator = protoChunk.getBlockEntities().values().iterator();
-
- while (iterator.hasNext()) {
-@@ -248,7 +254,7 @@ public class LevelChunk extends ChunkAccess {
- }
- }
-
-- this.skyLightSources = protoChunk.skyLightSources;
-+ // Paper - starlight - remove skyLightSources
- this.setLightCorrect(protoChunk.isLightCorrect());
- this.unsaved = true;
- this.needsDecoration = true; // CraftBukkit
-@@ -437,7 +443,7 @@ public class LevelChunk extends ChunkAccess {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
- gameprofilerfiller.push("updateSkyLightSources");
-- this.skyLightSources.update(this, j, i, l);
-+ // Paper - starlight - remove skyLightSources
- gameprofilerfiller.popPush("queueCheckLight");
- this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
- gameprofilerfiller.pop();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-index 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -155,7 +155,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- return this.get(this.strategy.getIndex(x, y, z));
- }
-
-- protected T get(int index) {
-+ public T get(int index) { // Paper - public
- PalettedContainer.Data<T> data = this.data;
- return data.palette.valueFor(data.storage.get(index));
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-index bcc70883d23d38c408130ffe778205e371ff4e8a..576ae0cb138b265c8a3995de7b5ebc827d50949d 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess {
- }
-
- if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) {
-- this.skyLightSources.update(this, m, j, o);
-+ // Paper - starlight - remove skyLightSources
- this.lightEngine.checkBlock(pos);
- }
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-index ae992aeb8b836e8c2e5bab338ae46cc31c317245..95318092f8281d98132d1d3ceb4a5c36cf32eb05 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-@@ -117,6 +117,18 @@ public class ChunkStatus {
- private final ChunkType chunkType;
- private final EnumSet<Heightmap.Types> heightmapsAfter;
-
-+ // Paper start - starlight
-+ public static ChunkStatus getStatus(String name) {
-+ try {
-+ // We need this otherwise we return EMPTY for invalid names
-+ ResourceLocation key = new ResourceLocation(name);
-+ return BuiltInRegistries.CHUNK_STATUS.getOptional(key).orElse(null);
-+ } catch (Exception ex) {
-+ return null; // invalid name
-+ }
-+ }
-+ // Paper end - starlight
-+
- private static ChunkStatus register(
- String id,
- @Nullable ChunkStatus previous,
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-index 88f0aca2da0e14ed5ec0513944fa0ba28b73b5d1..01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-@@ -90,6 +90,14 @@ public class ChunkSerializer {
- private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion();
- private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
- // Paper end - Do not let the server load chunks from newer versions
-+ // Paper start - replace light engine impl
-+ private static final int STARLIGHT_LIGHT_VERSION = 9;
-+
-+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
-+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
-+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
-+ // Paper end - replace light engine impl
-+
- public ChunkSerializer() {}
-
- // Paper start - guard against serializing mismatching coordinates
-@@ -121,19 +129,26 @@ public class ChunkSerializer {
- }
-
- UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY;
-- boolean flag = nbt.getBoolean("isLightOn");
-+ boolean flag = getStatus(nbt) != null && getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper
- ListTag nbttaglist = nbt.getList("sections", 10);
- int i = world.getSectionsCount();
- LevelChunkSection[] achunksection = new LevelChunkSection[i];
- boolean flag1 = world.dimensionType().hasSkyLight();
- ServerChunkCache chunkproviderserver = world.getChunkSource();
- LevelLightEngine levellightengine = chunkproviderserver.getLightEngine();
-+ // Paper start
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world);
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world);
-+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world);
-+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world);
-+ boolean canReadSky = world.dimensionType().hasSkyLight();
-+ // Paper end
- Registry<Biome> iregistry = world.registryAccess().registryOrThrow(Registries.BIOME);
- Codec<PalettedContainer<Holder<Biome>>> codec = ChunkSerializer.makeBiomeCodecRW(iregistry); // CraftBukkit - read/write
- boolean flag2 = false;
-
- for (int j = 0; j < nbttaglist.size(); ++j) {
-- CompoundTag nbttagcompound1 = nbttaglist.getCompound(j);
-+ CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound1; // Paper
- byte b0 = nbttagcompound1.getByte("Y");
- int k = world.getSectionIndexFromSectionY(b0);
-
-@@ -169,19 +184,39 @@ public class ChunkSerializer {
- boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
- boolean flag4 = flag1 && nbttagcompound1.contains("SkyLight", 7);
-
-- if (flag3 || flag4) {
-- if (!flag2) {
-- levellightengine.retainData(chunkPos, true);
-- flag2 = true;
-- }
--
-+ // Paper start - rewrite the light engine
-+ if (flag) {
-+ try {
-+ int y = sectionData.getByte("Y");
-+ // Paper end - rewrite the light engine
- if (flag3) {
-- levellightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight")));
-+ // Paper start - rewrite the light engine
-+ // this is where our diff is
-+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
-+ // Paper end - rewrite the light engine
- }
-
- if (flag4) {
-- levellightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight")));
-+ // Paper start - rewrite the light engine
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
-+ } else if (flag1) {
-+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
-+ // Paper end - rewrite the light engine
-+ }
-+
-+ // Paper start - rewrite the light engine
-+ } catch (Exception ex) {
-+ LOGGER.warn("Failed to load light data for chunk " + chunkPos + " in world '" + world.getWorld().getName() + "', light will be regenerated", ex);
-+ flag = false;
- }
-+ // Paper end - rewrite light engine
- }
- }
-
-@@ -211,6 +246,8 @@ public class ChunkSerializer {
- }, chunkPos);
-
- object1 = new LevelChunk(world.getLevel(), chunkPos, chunkconverter, levelchunkticks, levelchunkticks1, l, achunksection, ChunkSerializer.postLoadChunk(world, nbt), blendingdata);
-+ ((LevelChunk)object1).setBlockNibbles(blockNibbles); // Paper - replace light impl
-+ ((LevelChunk)object1).setSkyNibbles(skyNibbles); // Paper - replace light impl
- } else {
- ProtoChunkTicks<Block> protochunkticklist = ProtoChunkTicks.load(nbt.getList("block_ticks", 10), (s) -> {
- return BuiltInRegistries.BLOCK.getOptional(ResourceLocation.tryParse(s));
-@@ -219,6 +256,8 @@ public class ChunkSerializer {
- return BuiltInRegistries.FLUID.getOptional(ResourceLocation.tryParse(s));
- }, chunkPos);
- ProtoChunk protochunk = new ProtoChunk(chunkPos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, iregistry, blendingdata);
-+ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl
-+ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl
-
- object1 = protochunk;
- protochunk.setInhabitedTime(l);
-@@ -340,6 +379,12 @@ public class ChunkSerializer {
- // CraftBukkit end
-
- public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
-+ // Paper start - rewrite light impl
-+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world);
-+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world);
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles();
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles();
-+ // Paper end - rewrite light impl
- ChunkPos chunkcoordintpair = chunk.getPos();
- CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag());
-
-@@ -389,11 +434,14 @@ public class ChunkSerializer {
- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
- int j = chunk.getSectionIndexFromSectionY(i);
- boolean flag1 = j >= 0 && j < achunksection.length;
-- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-+ // Paper - replace light engine
-
-- if (flag1 || nibblearray != null || nibblearray1 != null) {
-- CompoundTag nbttagcompound1 = new CompoundTag();
-+ // Paper start - replace light engine
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
-+ if (flag1 || blockNibble != null || skyNibble != null) {
-+ // Paper end - replace light engine
-+ CompoundTag nbttagcompound1 = new CompoundTag(); CompoundTag section = nbttagcompound1; // Paper
-
- if (flag1) {
- LevelChunkSection chunksection = achunksection[j];
-@@ -402,13 +450,27 @@ public class ChunkSerializer {
- nbttagcompound1.put("biomes", (Tag) codec.encodeStart(NbtOps.INSTANCE, chunksection.getBiomes()).getOrThrow());
- }
-
-- if (nibblearray != null && !nibblearray.isEmpty()) {
-- nbttagcompound1.putByteArray("BlockLight", nibblearray.getData());
-+ // Paper start
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+
-+ if (blockNibble != null) {
-+ if (blockNibble.data != null) {
-+ section.putByteArray("BlockLight", blockNibble.data);
-+ }
-+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
- }
-
-- if (nibblearray1 != null && !nibblearray1.isEmpty()) {
-- nbttagcompound1.putByteArray("SkyLight", nibblearray1.getData());
-+ if (skyNibble != null) {
-+ if (skyNibble.data != null) {
-+ section.putByteArray("SkyLight", skyNibble.data);
-+ }
-+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
- }
-+ // Paper end
-
- if (!nbttagcompound1.isEmpty()) {
- nbttagcompound1.putByte("Y", (byte) i);
-@@ -419,7 +481,8 @@ public class ChunkSerializer {
-
- nbttagcompound.put("sections", nbttaglist);
- if (flag) {
-- nbttagcompound.putBoolean("isLightOn", true);
-+ nbttagcompound.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper
-+ nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_)
- }
-
- ListTag nbttaglist1 = new ListTag();
-@@ -493,6 +556,17 @@ public class ChunkSerializer {
- }));
- }
-
-+ // Paper start
-+ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) {
-+ if (compound == null) {
-+ return null;
-+ }
-+
-+ // Note: Copied from below
-+ return ChunkStatus.getStatus(compound.getString("Status"));
-+ }
-+ // Paper end
-+
- public static ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) {
- return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkType.PROTOCHUNK;
- }
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 44ed6bd76fb9e81f6c0d99fe46173685dbbfe2a7..7aee9f6b143c89cf8d65ca55eeda808152b4dd26 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -507,12 +507,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- }
- }
-
-- for (final ChunkPos pos : chunksToRelight) {
-- final ChunkAccess chunk = serverChunkCache.getChunk(pos.x, pos.z, false);
-- if (chunk != null) {
-- serverChunkCache.getLightEngine().lightChunk(chunk, false);
-- }
-- }
-+ serverChunkCache.getLightEngine().relight(chunksToRelight, pos -> {}, relit -> {}); // Paper - Starlight
-
- return true;
- // Paper end - implement regenerate chunk method