diff --git a/leaf_notes.txt b/leaf_notes.txt
new file mode 100644
index 0000000000..7461a327b9
--- /dev/null
+++ b/leaf_notes.txt
@@ -0,0 +1,32 @@
+- Starlight fixlight command + method on light engine (note: add to mod to, after done this)
+- note: for paper, the chunk debug command
+- rebase IntervalledCounter into util patch
+- mcutil diff
+- paper debug chunks --async in DedicatedServer
+- TODO keep around region file lock?
+- mcutil#getTicketLevelFor is wrong, just delete it later
+- in the mod:
+ - ChunkHolder
+ - isReadyForSaving overwrite
+ - remove state fields in mod
+ - addSaveDependency overwrite
+ - ChunkMap
+ - pendingUnloads/pendingGenerationTasks/unloadQueue/ field destroy
+ - DistanceManager
+ - getTickets/dumpTickets/tickingTracker/ overwrite
+ - GenerationChunkHolder
+ - remove state fields in mod
+ - rescheduleChunkTask/failAndClearPendingFuturesBetween/failAndClearPendingFuture/completeFuture/
+ findHighestStatusWithPendingFuture/acquireStatusBump/isStatusDisallowed/ overwrite
+ - LayerLightEngine
+ - getDebugSectionType overwrite
+ - ThreadedLayerLightEngine
+ - waitForPendingTasks overwrite
+
+on another note, clean up mcutils...
+
+later, run a diff compared to the mod and move all of the diff to separate classes
+apply todo in levelmixin
+
+to fix later:
+- Change loadedChunkMap in ServerChunkCache to use concurrent long map
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/server/0988-Chunk-System-Starlight-from-Moonrise.patch b/patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch
new file mode 100644
index 0000000000..d3df55cb84
--- /dev/null
+++ b/patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch
@@ -0,0 +1,28736 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf
+Date: Fri, 14 Jun 2024 11:57:26 -0700
+Subject: [PATCH] Chunk System + Starlight from Moonrise
+
+See https://github.com/Tuinity/Moonrise
+
+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..ba68998f6ef57b24c72fd833bd7de440de9501cc
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.list;
++
++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;
++
++// list with O(1) remove & contains
++
++/**
++ * @author Spottedleaf
++ */
++public final class EntityList implements Iterable {
++
++ protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
++ {
++ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ protected static final Entity[] EMPTY_LIST = new Entity[0];
++
++ protected Entity[] entities = EMPTY_LIST;
++ protected int count;
++
++ public int size() {
++ return this.count;
++ }
++
++ public boolean contains(final Entity entity) {
++ return this.entityToIndex.containsKey(entity.getId());
++ }
++
++ public boolean remove(final Entity entity) {
++ final int index = this.entityToIndex.remove(entity.getId());
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // 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;
++ }
++
++ public boolean add(final Entity entity) {
++ final int count = this.count;
++ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ 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 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 Entity getUnchecked(final int index) {
++ return this.entities[index];
++ }
++
++ public Entity[] getRawData() {
++ return this.entities;
++ }
++
++ public void clear() {
++ this.entityToIndex.clear();
++ Arrays.fill(this.entities, 0, this.count, null);
++ this.count = 0;
++ }
++
++ @Override
++ public Iterator iterator() {
++ return new Iterator() {
++
++ Entity lastRet;
++ int current;
++
++ @Override
++ public boolean hasNext() {
++ return this.current < EntityList.this.count;
++ }
++
++ @Override
++ public Entity next() {
++ if (this.current >= EntityList.this.count) {
++ throw new NoSuchElementException();
++ }
++ return this.lastRet = EntityList.this.entities[this.current++];
++ }
++
++ @Override
++ public void remove() {
++ final Entity lastRet = this.lastRet;
++
++ if (lastRet == null) {
++ throw new IllegalStateException();
++ }
++ this.lastRet = null;
++
++ 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;
++
++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;
++
++public final class IBlockDataList {
++
++ private static final GlobalPalette 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);
++ }
++
++ private static final long[] EMPTY_LIST = new long[0];
++
++ private long[] byIndex = EMPTY_LIST;
++ private int size;
++
++ public static int getLocationKey(final int x, final int y, final int z) {
++ return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
++ }
++
++ public static BlockState getBlockDataFromRaw(final long raw) {
++ return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
++ }
++
++ public static int getIndexFromRaw(final long raw) {
++ return (int)(raw & 0xFFFF);
++ }
++
++ public static int getLocationFromRaw(final long raw) {
++ return (int)((raw >>> 16) & 0xFFFF);
++ }
++
++ 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);
++ }
++
++ public static long setIndexRawValues(final long value, final int index) {
++ return value & ~(0xFFFF) | (index);
++ }
++
++ public long add(final int x, final int y, final int z, final BlockState data) {
++ return this.add(getLocationKey(x, y, z), data);
++ }
++
++ public long add(final int location, final BlockState data) {
++ final long curr = this.map.get((short)location);
++
++ 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 long remove(final int x, final int y, final int z) {
++ return this.remove(getLocationKey(x, y, z));
++ }
++
++ 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;
++ }
++
++ // 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;
++
++ return ret;
++ }
++
++ public int size() {
++ return this.size;
++ }
++
++ public long getRaw(final int index) {
++ return this.byIndex[index];
++ }
++
++ public int getLocation(final int index) {
++ return getLocationFromRaw(this.getRaw(index));
++ }
++
++ public BlockState getData(final int index) {
++ return getBlockDataFromRaw(this.getRaw(index));
++ }
++
++ public void clear() {
++ this.size = 0;
++ this.map.clear();
++ }
++
++ 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;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
++
++public final class IteratorSafeOrderedReferenceSet {
++
++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
++
++ private final Reference2IntLinkedOpenHashMap indexMap;
++ private int firstInvalidIndex = -1;
++
++ /* list impl */
++ private E[] listElements;
++ private int listSize;
++
++ private final double maxFragFactor;
++
++ 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 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");
++ }
++ }
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++ }
++
++ check.clear();
++ iterated = 0;
++ for (final java.util.Iterator 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)");
++ }
++ }
++
++ 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);
++ }
++
++ public int createRawIterator() {
++ ++this.iteratorCount;
++ if (this.indexMap.isEmpty()) {
++ return -1;
++ } else {
++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++ }
++ }
++
++ 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;
++ }
++ }
++
++ return -1;
++ }
++
++ public void finishRawIterator() {
++ if (--this.iteratorCount == 0) {
++ if (this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ }
++ }
++
++ 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 boolean contains(final E element) {
++ return this.indexMap.containsKey(element);
++ }
++
++ public boolean add(final E element) {
++ final int listSize = this.listSize;
++
++ final int previous = this.indexMap.putIfAbsent(element, listSize);
++ if (previous != -1) {
++ return false;
++ }
++
++ if (listSize >= this.listElements.length) {
++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
++ }
++ this.listElements[listSize] = element;
++ this.listSize = listSize + 1;
++
++ //this.check();
++ return true;
++ }
++
++ private void defrag() {
++ if (this.firstInvalidIndex < 0) {
++ return; // nothing to do
++ }
++
++ if (this.indexMap.isEmpty()) {
++ Arrays.fill(this.listElements, 0, this.listSize, null);
++ this.listSize = 0;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ return;
++ }
++
++ final E[] backingArray = this.listElements;
++
++ int lastValidIndex;
++ java.util.Iterator> 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() {
++ @Override
++ public int getIntValue() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public int setValue(int i) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public E getKey() {
++ return key;
++ }
++ });
++ }
++
++ while (iterator.hasNext()) {
++ final Reference2IntMap.Entry entry = iterator.next();
++
++ final int newIndex = lastValidIndex++;
++ backingArray[newIndex] = entry.getKey();
++ entry.setValue(newIndex);
++ }
++
++ // 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 iterator() {
++ return this.iterator(0);
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator 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 unsafeIterator() {
++ return this.unsafeIterator(0);
++ }
++ public java.util.Iterator unsafeIterator(final int flags) {
++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public static interface Iterator extends java.util.Iterator {
++
++ public void finishedIterating();
++
++ }
++
++ private static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator {
++
++ private final IteratorSafeOrderedReferenceSet 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 set, final boolean canFinish, final int maxIndex) {
++ this.set = set;
++ this.canFinish = canFinish;
++ this.maxIndex = maxIndex;
++ }
++
++ @Override
++ public boolean hasNext() {
++ if (this.finished) {
++ return false;
++ }
++ if (this.pendingValue != null) {
++ return true;
++ }
++
++ 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;
++ }
++ }
++
++ this.nextIndex = index;
++ return false;
++ }
++
++ @Override
++ public E next() {
++ if (!this.hasNext()) {
++ throw new NoSuchElementException();
++ }
++ final E ret = this.pendingValue;
++
++ this.pendingValue = null;
++ this.lastReturned = ret;
++
++ return ret;
++ }
++
++ @Override
++ 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/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..93e8c8134da8ee1a9b777c708f992922a1a7de8b
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+@@ -0,0 +1,135 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++
++public final class ReferenceList implements Iterable {
++
++ private final Reference2IntOpenHashMap referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
++ {
++ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ private static final Object[] EMPTY_LIST = new Object[0];
++
++ private E[] references;
++ private int count;
++
++ public ReferenceList() {
++ this((E[])EMPTY_LIST, 0);
++ }
++
++ public ReferenceList(final E[] array, final int count) {
++ this.references = array;
++ this.count = count;
++ }
++
++ public int size() {
++ return this.count;
++ }
++
++ public boolean contains(final E obj) {
++ return this.referenceToIndex.containsKey(obj);
++ }
++
++ public boolean remove(final E obj) {
++ final int index = this.referenceToIndex.removeInt(obj);
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // move the object at the end to this index
++ final int endIndex = --this.count;
++ final E end = (E)this.references[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.referenceToIndex.put(end, index); // update index
++ }
++ this.references[index] = end;
++ this.references[endIndex] = null;
++
++ return true;
++ }
++
++ public boolean add(final E obj) {
++ final int count = this.count;
++ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ E[] list = this.references;
++
++ if (list.length == count) {
++ // resize required
++ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = obj;
++ this.count = count + 1;
++
++ return true;
++ }
++
++ public E getChecked(final int index) {
++ if (index < 0 || index >= this.count) {
++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
++ }
++ return this.references[index];
++ }
++
++ public E getUnchecked(final int index) {
++ return this.references[index];
++ }
++
++ public Object[] getRawData() {
++ return this.references;
++ }
++
++ public E[] getRawDataUnchecked() {
++ return this.references;
++ }
++
++ public void clear() {
++ this.referenceToIndex.clear();
++ Arrays.fill(this.references, 0, this.count, null);
++ this.count = 0;
++ }
++
++ @Override
++ public Iterator iterator() {
++ return new Iterator<>() {
++ private E lastRet;
++ private int current;
++
++ @Override
++ public boolean hasNext() {
++ return this.current < ReferenceList.this.count;
++ }
++
++ @Override
++ public E next() {
++ if (this.current >= ReferenceList.this.count) {
++ throw new NoSuchElementException();
++ }
++ return this.lastRet = ReferenceList.this.references[this.current++];
++ }
++
++ @Override
++ public void remove() {
++ final E lastRet = this.lastRet;
++
++ if (lastRet == null) {
++ throw new IllegalStateException();
++ }
++ this.lastRet = null;
++
++ ReferenceList.this.remove(lastRet);
++ --this.current;
++ }
++ };
++ }
++}
+diff --git a/src/main/java/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;
++
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.Comparator;
++
++public final class SortedList {
++
++ 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 int insertIdx(final E[] elements, final E element, final Comparator comparator,
++ int start, int end) {
++ while (start <= end) {
++ final int middle = (start + end) >>> 1;
++
++ 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 int size() {
++ return this.count;
++ }
++
++ public boolean isEmpty() {
++ return this.count == 0;
++ }
++
++ 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;
++
++ 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;
++ }
++ }
++ }
++
++ public E get(final int idx) {
++ if (idx < 0 || idx >= this.count) {
++ throw new IndexOutOfBoundsException(idx);
++ }
++ return this.elements[idx];
++ }
++
++
++ public E remove(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ final Comparator super E> comparator = this.comparator;
++
++ final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
++ if (idx < 0) {
++ return null;
++ }
++
++ final int last = this.count - 1;
++ this.count = last;
++
++ final E ret = elements[idx];
++
++ System.arraycopy(elements, idx + 1, elements, idx, last - idx);
++
++ elements[last] = null;
++
++ 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;
++
++import it.unimi.dsi.fastutil.ints.Int2IntFunction;
++
++import java.util.Arrays;
++
++public class Int2IntArraySortedMap {
++
++ protected int[] key;
++ protected int[] val;
++ protected int size;
++
++ 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;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return 0;
++ }
++
++ 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;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ 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];
++ }
++
++ 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;
++
++import java.util.Arrays;
++import java.util.function.IntFunction;
++
++public class Int2ObjectArraySortedMap {
++
++ protected int[] key;
++ protected V[] val;
++ protected int size;
++
++ public Int2ObjectArraySortedMap() {
++ this.key = new int[8];
++ this.val = (V[])new Object[8];
++ }
++
++ 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);
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
++ }
++
++ public V computeIfAbsent(final int key, final IntFunction 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.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ 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 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 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 {
++
++ protected long[] key;
++ protected int[] val;
++ protected int size;
++
++ public Long2IntArraySortedMap() {
++ this.key = new long[8];
++ this.val = new int[8];
++ }
++
++ 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;
++ }
++ 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 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];
++ }
++ 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 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 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 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;
++
++import java.util.Arrays;
++import java.util.function.LongFunction;
++
++public class Long2ObjectArraySortedMap {
++
++ protected long[] key;
++ protected V[] val;
++ protected int size;
++
++ public Long2ObjectArraySortedMap() {
++ this.key = new long[8];
++ this.val = (V[])new Object[8];
++ }
++
++ 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;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
++ }
++
++ public V computeIfAbsent(final long key, final LongFunction 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;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
++ }
++
++ 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 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;
++
++import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
++import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
++
++public final class SynchronisedLong2BooleanMap {
++ private final Long2BooleanLinkedOpenHashMap map = new 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 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;
++ }
++ }
++}
+\ 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;
++
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import java.util.function.BiFunction;
++
++public final class SynchronisedLong2ObjectMap {
++ private final Long2ObjectLinkedOpenHashMap map = new 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 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 final class AllocatingRateLimiter {
++
++ // max difference granularity in ns
++ private final long maxGranularity;
++
++ 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;
++
++ public AllocatingRateLimiter(final long maxGranularity) {
++ this.maxGranularity = maxGranularity;
++ }
++
++ public void reset(final long time) {
++ this.allocation = 0.0;
++ this.lastAllocationUpdate = time;
++ this.takeCarry = 0.0;
++ this.lastTakeUpdate = time;
++ }
++
++ // 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;
++
++ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
++ }
++
++ public long previewAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
++
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ 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;
++ }
++
++ double ret = this.takeCarry;
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++ this.lastTakeUpdate = time;
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ ret += take;
++ this.allocation -= take;
++
++ final long retInteger = (long)Math.floor(ret);
++ this.takeCarry = ret - (double)retInteger;
++
++ 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;
++
++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 final class Delayed26WayDistancePropagator3D {
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // 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);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed26WayDistancePropagator3D() {
++ this(null);
++ }
++
++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
++ }
++
++ public int getLevel(final int x, final int y, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void setSource(final int x, final int y, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
++ }
++
++ 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);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int y, final int z) {
++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // 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();
++ }
++ }
++ 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;
++
++ 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);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ 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);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ 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);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ 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
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // 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;
++ }
++
++ 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 (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
++
++ 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);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // 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);
++ }
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ 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 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 = 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.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ // 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;
++
++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 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;
++ }
++ }
++ }
++ }
++
++ if (expect != got) {
++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
++ }
++ }
++
++ static class Ticket {
++
++ int x;
++ int z;
++
++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty
++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
++
++ }
++
++ public static void main(final String[] args) {
++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() {
++ @Override
++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(Ticket object) {
++ return object.empty;
++ }
++ };
++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
++
++ 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);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(0, 0); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ java.util.List 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();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ 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);
++ }
++ }
++
++ // 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);
++ }
++ }
++
++ // 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();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++
++ // 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);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(offX, offZ); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ java.util.List 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();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ 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);
++ }
++ }
++
++ // 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);
++ }
++ }
++
++ // 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();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++ }
++ */
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // 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);
++
++ }
++
++ protected final LevelChangeCallback changeCallback;
++
++ public Delayed8WayDistancePropagator2D() {
++ this(null);
++ }
++
++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
++ }
++
++ 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));
++ }
++
++ public void setSource(final int x, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkKey(x, z), level);
++ }
++
++ 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);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
++ }
++
++ public void removeSource(final int x, final int z) {
++ this.removeSource(CoordinateUtils.getChunkKey(x, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
++ }
++
++ // 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;
++
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
++
++ 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);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
++
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ 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
++ }
++ }
++
++ this.updatedSources.clear();
++
++ // 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;
++ }
++
++ 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 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 (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
++
++ 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);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ 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.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ 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();
++ }
++
++ protected static final class LevelMap extends Long2ByteOpenHashMap {
++ public LevelMap() {
++ super();
++ }
++
++ public LevelMap(final int expected, final float loadFactor) {
++ super(expected, loadFactor);
++ }
++
++ // 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);
++ }
++ }
++ }
++
++ // copied from superclass
++ private void insert(final int pos, final long k, final byte v) {
++ if (pos == this.n) {
++ this.containsNullKey = true;
++ }
++
++ this.key[pos] = k;
++ this.value[pos] = v;
++ if (this.size++ >= this.maxFill) {
++ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++ }
++ }
++
++ // 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;
++ }
++ }
++
++ // 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);
++ }
++ }
++
++ // 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);
++ }
++ }
++
++ // 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;
++ }
++ }
++ }
++ }
++
++ 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 t;
++ }
++ }
++
++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
++
++ /**
++ * 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;
++ }
++
++ 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;
++
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++
++public abstract class SingleUserAreaMap {
++
++ private static final int NOT_SET = Integer.MIN_VALUE;
++
++ private final T parameter;
++ private int lastChunkX = NOT_SET;
++ private int lastChunkZ = NOT_SET;
++ private int distance = NOT_SET;
++
++ public SingleUserAreaMap(final T parameter) {
++ this.parameter = parameter;
++ }
++
++ /* math sign function except 0 returns 1 */
++ protected static int sign(int val) {
++ return 1 | (val >> (Integer.SIZE - 1));
++ }
++
++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ 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;
++
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.addCallback(parameter, cx, cz);
++ }
++ }
++ }
++
++ 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;
++
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.removeCallback(parameter, cx, cz);
++ }
++ }
++ }
++
++ 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;
++
++ this.addToNew(this.parameter, chunkX, chunkZ, distance);
++
++ return true;
++ }
++
++ 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;
++ }
++
++ this.lastChunkX = toX;
++ this.lastChunkZ = toZ;
++ this.distance = newViewDistance;
++
++ final T parameter = this.parameter;
++
++
++ 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);
++ }
++ }
++ }
++
++ // add loop
++
++ 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);
++ }
++ }
++ }
++
++ return true;
++ }
++
++ // 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
++
++ // same view distance
++
++ // 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
++
++ // 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.
++
++ // 4 points of the rectangle
++ int maxX; // exclusive
++ int minX; // inclusive
++ int maxZ; // exclusive
++ int minZ; // inclusive
++
++ if (dx != 0) {
++ // handle right addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX + (oldViewDistance * right) + right; // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dz != 0) {
++ // handle up addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = toX - (oldViewDistance * right); // inclusive
++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ 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
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dz != 0) {
++ // handle down removal
++
++ maxX = fromX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = toZ - (oldViewDistance * up); // exclusive
++ minZ = fromZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ return true;
++ }
++
++ 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;
++ }
++
++ 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;
++
++import java.util.Collection;
++
++public final class OptimizedSmallEnumSet> {
++
++ private final Class enumClass;
++ private long backingSet;
++
++ public OptimizedSmallEnumSet(final Class 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;
++ }
++
++ public boolean addUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
++
++ final long prev = this.backingSet;
++ this.backingSet = prev | key;
++
++ return (prev & key) == 0;
++ }
++
++ public boolean removeUnchecked(final E element) {
++ final int ordinal = element.ordinal();
++ final long key = 1L << ordinal;
++
++ final long prev = this.backingSet;
++ this.backingSet = prev & ~key;
++
++ return (prev & key) != 0;
++ }
++
++ public void clear() {
++ this.backingSet = 0L;
++ }
++
++ public int size() {
++ return Long.bitCount(this.backingSet);
++ }
++
++ public void addAllUnchecked(final Collection enums) {
++ for (final E element : enums) {
++ if (element == null) {
++ throw new NullPointerException("Null element");
++ }
++ this.backingSet |= (1L << element.ordinal());
++ }
++ }
++
++ public long getBackingSet() {
++ return this.backingSet;
++ }
++
++ public boolean hasCommonElements(final OptimizedSmallEnumSet other) {
++ return (other.backingSet & this.backingSet) != 0;
++ }
++
++ 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;
++
++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;
++
++public final class CoordinateUtils {
++
++ // 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 ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(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));
++ }
++
++ public static int getBlockX(final Vec3 pos) {
++ return Mth.floor(pos.x);
++ }
++
++ public static int getBlockY(final Vec3 pos) {
++ return Mth.floor(pos.y);
++ }
++
++ public static int getBlockZ(final Vec3 pos) {
++ return Mth.floor(pos.z);
++ }
++
++ public static int getChunkX(final Vec3 pos) {
++ return Mth.floor(pos.x) >> 4;
++ }
++
++ public static int getChunkY(final Vec3 pos) {
++ return Mth.floor(pos.y) >> 4;
++ }
++
++ public static int getChunkZ(final Vec3 pos) {
++ return Mth.floor(pos.z) >> 4;
++ }
++
++ 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();
++ }
++
++ 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;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = bitset[++bitsetIdx];
++ }
++ }
++
++ // 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
++
++ 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;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = ~bitset[++bitsetIdx];
++ }
++ }
++
++ // 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();
++ }
++
++ if (from == to) {
++ return;
++ }
++
++ --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;
++ }
++
++ bitset[toBitsetIdx] &= keepLast;
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
++ return firstClear(bitset, from, to) == -1;
++ }
++
++
++ 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;
++
++public final class MixinWorkarounds {
++
++ // 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();
++ }
++
++}
+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;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++
++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));
++
++ int workerThreads = chunkSystem.workerThreads;
++
++ 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;
++ }
++
++ 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;
++
++public final class MoonriseConstants {
++
++ public static final int MAX_VIEW_DISTANCE = 32;
++
++ private MoonriseConstants() {}
++
++}
+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;
++
++import net.minecraft.world.level.Level;
++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;
++ }
++
++ public static String getWorldName(final Level world) {
++ if (world == null) {
++ return "null world";
++ }
++ return world.getWorld().getName();
++ }
++
++ 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;
++
++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;
++
++public final class ChunkSystem {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++ scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++ }
++
++ 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);
++ }
++
++ 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 onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
++ }
++
++ // Paper - rewrite chunk system
++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }
++
++ 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 onComplete) {
++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }
++
++ public static List getVisibleChunkHolders(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
++ }
++
++ public static List getUpdatingChunkHolders(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
++ }
++
++ public static int getVisibleChunkHolderCount(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++ }
++
++ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
++ }
++
++ public static boolean hasAnyChunkHolders(final ServerLevel level) {
++ return getUpdatingChunkHolderCount(level) != 0;
++ }
++
++ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd(level, entity);
++ }
++
++ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(level, holder);
++ }
++
++ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(level, holder);
++ }
++
++ 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
++ }
++
++ 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
++ }
++
++ 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();
++ }
++
++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(chunk, holder);
++ }
++
++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, holder);
++ }
++
++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++ // TODO move hook
++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(chunk, holder);
++ }
++
++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
++ return null;
++ }
++
++ public static int getSendViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
++ }
++
++ public static int getLoadViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
++ }
++
++ public static int getTickViewDistance(final ServerPlayer player) {
++ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
++ }
++
++ public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
++ }
++
++ public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
++ }
++
++ public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
++ }
++
++ 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;
++
++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();
++ }
++
++ 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);
++ }
++
++ 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());
++ }
++
++ 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());
++ }
++
++ 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 static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) {
++ return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk);
++ }
++
++ 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 boolean forceNoSave(final ChunkAccess chunk) {
++ // support for CB chunk mustNotSave
++ return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave;
++ }
++
++ 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;
++ }
++
++ 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;
++
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.Tag;
++
++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;
++
++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 interface ChunkSystemEntity {
++
++ public boolean moonrise$isHardColliding();
++
++ // for mods to override
++ public default boolean moonrise$isHardCollidingUncached() {
++ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith();
++ }
++
++ public FullChunkStatus moonrise$getChunkStatus();
++
++ public void moonrise$setChunkStatus(final FullChunkStatus status);
++
++ public int moonrise$getSectionX();
++
++ public void moonrise$setSectionX(final int x);
++
++ public int moonrise$getSectionY();
++
++ public void moonrise$setSectionY(final int y);
++
++ public int moonrise$getSectionZ();
++
++ public void moonrise$setSectionZ(final int z);
++
++ public boolean moonrise$isUpdatingSectionStatus();
++
++ public void moonrise$setUpdatingSectionStatus(final boolean to);
++
++ 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;
++
++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;
++
++}
+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..c833f78d083b8f661087471c35bc90f65af1b525
+--- /dev/null
++++ 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 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.atomic.AtomicInteger;
++import java.util.function.BiConsumer;
++import java.util.function.Consumer;
++import java.util.function.Function;
++
++/**
++ * Prioritised RegionFile I/O executor, responsible for all RegionFile access.
++ *
++ * All functions provided are MT-Safe, however certain ordering constraints are recommended:
++ *
++ * Chunk saves may not occur for unloaded chunks.
++ *
++ *
++ * Tasks must be scheduled on the chunk scheduler thread.
++ *
++ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
++ *
++ */
++public final class RegionFileIOThread extends PrioritisedQueueExecutorThread {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class);
++
++ /**
++ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure
++ * getControllerFor is updated.
++ */
++ public static enum RegionFileType {
++ CHUNK_DATA,
++ POI_DATA,
++ ENTITY_DATA;
++ }
++
++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
++
++ 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);
++ }
++ }
++
++ /**
++ * Collects regionfile data for a certain chunk.
++ */
++ public static final class RegionFileData {
++
++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length];
++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length];
++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length];
++
++ /**
++ * Sets the result associated with the specified regionfile type. Note that
++ * results can only be set once per regionfile type.
++ *
++ * @param type The regionfile type.
++ * @param data The result to set.
++ */
++ public void setData(final RegionFileType type, final CompoundTag data) {
++ final int index = type.ordinal();
++
++ if (this.hasResult[index]) {
++ throw new IllegalArgumentException("Result already exists for type " + type);
++ }
++ this.hasResult[index] = true;
++ this.data[index] = data;
++ }
++
++ /**
++ * Sets the result associated with the specified regionfile type. Note that
++ * results can only be set once per regionfile type.
++ *
++ * @param type The regionfile type.
++ * @param throwable The result to set.
++ */
++ public void setThrowable(final RegionFileType type, final Throwable throwable) {
++ final int index = type.ordinal();
++
++ if (this.hasResult[index]) {
++ throw new IllegalArgumentException("Result already exists for type " + type);
++ }
++ this.hasResult[index] = true;
++ this.throwables[index] = throwable;
++ }
++
++ /**
++ * Returns whether there is a result for the specified regionfile type.
++ *
++ * @param type Specified regionfile type.
++ *
++ * @return Whether a result exists for {@code type}.
++ */
++ public boolean hasResult(final RegionFileType type) {
++ return this.hasResult[type.ordinal()];
++ }
++
++ /**
++ * Returns the data result for the regionfile type.
++ *
++ * @param type Specified regionfile type.
++ *
++ * @throws IllegalArgumentException If the result has not been set for {@code type}.
++ * @return The data result for the specified type. If the result is a {@code Throwable},
++ * then returns {@code null}.
++ */
++ public CompoundTag getData(final RegionFileType type) {
++ final int index = type.ordinal();
++
++ if (!this.hasResult[index]) {
++ throw new IllegalArgumentException("Result does not exist for type " + type);
++ }
++
++ return this.data[index];
++ }
++
++ /**
++ * Returns the throwable result for the regionfile type.
++ *
++ * @param type Specified regionfile type.
++ *
++ * @throws IllegalArgumentException If the result has not been set for {@code type}.
++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag},
++ * then returns {@code null}.
++ */
++ public Throwable getThrowable(final RegionFileType type) {
++ final int index = type.ordinal();
++
++ if (!this.hasResult[index]) {
++ throw new IllegalArgumentException("Result does not exist for type " + type);
++ }
++
++ return this.throwables[index];
++ }
++ }
++
++ private static final Object INIT_LOCK = new Object();
++
++ static RegionFileIOThread[] threads;
++
++ /* needs to be consistent given a set of parameters */
++ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
++ if (threads == null) {
++ throw new IllegalStateException("Threads not initialised");
++ }
++
++ final int regionX = chunkX >> 5;
++ final int regionZ = chunkZ >> 5;
++ final int typeOffset = type.ordinal();
++
++ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length];
++ }
++
++ /**
++ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified.
++ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted.
++ *
++ * @param wait Whether to wait until all tasks have completed.
++ */
++ public static void close(final boolean wait) {
++ for (int i = 0, len = threads.length; i < len; ++i) {
++ threads[i].close(false, true);
++ }
++ if (wait) {
++ RegionFileIOThread.flush();
++ }
++ }
++
++ public static long[] getExecutedTasks() {
++ final long[] ret = new long[threads.length];
++ for (int i = 0, len = threads.length; i < len; ++i) {
++ ret[i] = threads[i].getTotalTasksExecuted();
++ }
++
++ return ret;
++ }
++
++ public static long[] getTasksScheduled() {
++ final long[] ret = new long[threads.length];
++ for (int i = 0, len = threads.length; i < len; ++i) {
++ ret[i] = threads[i].getTotalTasksScheduled();
++ }
++ return ret;
++ }
++
++ public static void flush() {
++ for (int i = 0, len = threads.length; i < len; ++i) {
++ threads[i].waitUntilAllExecuted();
++ }
++ }
++
++ public static void 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
++
++ for (;;) {
++ final long[] executed = getExecutedTasks();
++ final long[] scheduled = getTasksScheduled();
++
++ long sum = 0;
++ for (int i = 0; i < executed.length; ++i) {
++ sum += scheduled[i] - executed[i];
++ }
++
++ if (sum <= totalTasksRemaining) {
++ break;
++ }
++
++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
++ }
++ }
++
++ /**
++ * Inits the executor with the specified number of threads.
++ *
++ * @param threads Specified number of threads.
++ */
++ public static void init(final int threads) {
++ synchronized (INIT_LOCK) {
++ if (RegionFileIOThread.threads != null) {
++ throw new IllegalStateException("Already initialised threads");
++ }
++
++ RegionFileIOThread.threads = new RegionFileIOThread[threads];
++
++ for (int i = 0; i < threads; ++i) {
++ RegionFileIOThread.threads[i] = new RegionFileIOThread(i);
++ RegionFileIOThread.threads[i].start();
++ }
++ }
++ }
++
++ 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);
++ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us
++ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
++ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr);
++ });
++ }
++
++ /**
++ * Returns whether the current thread is a regionfile I/O executor.
++ * @return Whether the current thread is a regionfile I/O executor.
++ */
++ public static boolean isRegionFileThread() {
++ return Thread.currentThread() instanceof RegionFileIOThread;
++ }
++
++ /**
++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid
++ * dumb plugins from taking away priority from threads we consider crucial.
++ * @return The priroity to use with blocking I/O on the current thread.
++ */
++ public static Priority getIOBlockingPriorityForCurrentThread() {
++ if (io.papermc.paper.util.TickThread.isTickThread()) {
++ return Priority.BLOCKING;
++ }
++ return Priority.HIGHEST;
++ }
++
++ /**
++ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type.
++ * Note that this does not copy the result, so do not modify the result returned.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param type Specified regionfile type.
++ *
++ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending.
++ */
++ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
++ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type);
++ }
++
++ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++ if (task == null) {
++ return null;
++ }
++
++ final CompoundTag ret = task.inProgressWrite;
++
++ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret;
++ }
++
++ /**
++ * Returns the priority for the specified regionfile type for the specified chunk.
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param type Specified regionfile type.
++ * @return The priority for the chunk
++ */
++ public static 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);
++ }
++
++ 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 Priority.COMPLETING;
++ }
++
++ return task.prioritisedTask.getPriority();
++ }
++
++ /**
++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should
++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
++ * priorities.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param priority New priority.
++ *
++ * @see #raisePriority(ServerLevel, int, int, Priority)
++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Priority priority) {
++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
++ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority);
++ }
++ }
++
++ /**
++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should
++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
++ * priorities.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param type Specified regionfile type.
++ * @param priority New priority.
++ *
++ * @see #raisePriority(ServerLevel, int, int, Priority)
++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
++ final 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 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);
++ }
++ }
++
++ /**
++ * Raises the priority for all regionfile types for the specified chunk.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param priority New priority.
++ *
++ * @see #setPriority(ServerLevel, int, int, Priority)
++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Priority priority) {
++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
++ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority);
++ }
++ }
++
++ /**
++ * Raises the priority for the specified regionfile type for the specified chunk.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param type Specified regionfile type.
++ * @param priority New priority.
++ *
++ * @see #setPriority(ServerLevel, int, int, Priority)
++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, Priority)
++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
++ final 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 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);
++ }
++ }
++
++ /**
++ * Lowers the priority for all regionfile types for the specified chunk.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param priority New priority.
++ *
++ * @see #raisePriority(ServerLevel, int, int, Priority)
++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #setPriority(ServerLevel, int, int, Priority)
++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Priority priority) {
++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
++ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority);
++ }
++ }
++
++ /**
++ * Lowers the priority for the specified regionfile type for the specified chunk.
++ *
++ * @param world Specified world.
++ * @param chunkX Specified chunk x.
++ * @param chunkZ Specified chunk z.
++ * @param type Specified regionfile type.
++ * @param priority New priority.
++ *
++ * @see #raisePriority(ServerLevel, int, int, Priority)
++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
++ * @see #setPriority(ServerLevel, int, int, Priority)
++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
++ */
++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
++ final 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 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);
++ }
++ }
++
++ /**
++ * Schedules the chunk data to be written asynchronously.
++ *
++ * Impl notes:
++ *
++ *
++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
++ * saves must be scheduled before a chunk is unloaded.
++ *
++ *
++ * Writes may be called concurrently, although only the "later" write will go through.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param data Chunk's data
++ * @param type The regionfile type to write to.
++ *
++ * @throws IllegalStateException If the file io thread has shutdown.
++ */
++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
++ final RegionFileType type) {
++ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
++ }
++
++ /**
++ * Schedules the chunk data to be written asynchronously.
++ *
++ * Impl notes:
++ *
++ *
++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
++ * saves must be scheduled before a chunk is unloaded.
++ *
++ *
++ * Writes may be called concurrently, although only the "later" write will go through.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param data Chunk's data
++ * @param type The regionfile type to write to.
++ * @param priority The minimum priority to schedule at.
++ *
++ * @throws IllegalStateException If the file io thread has shutdown.
++ */
++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
++ final RegionFileType type, final 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 Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++
++ final boolean[] created = new boolean[1];
++ 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
++
++ // create task
++ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority);
++ newTask.inProgressWrite = data;
++ created[0] = true;
++
++ return newTask;
++ }
++
++ taskRunning.inProgressWrite = data;
++
++ return taskRunning;
++ });
++
++ if (created[0]) {
++ task.prioritisedTask.queue();
++ } else {
++ task.prioritisedTask.raisePriority(priority);
++ }
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
++ * for single load.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
++ */
++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Consumer onComplete, final boolean intendingToBlock) {
++ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
++ * for single load.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ * @param priority The minimum priority to load the data at.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
++ */
++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Consumer onComplete, final boolean intendingToBlock,
++ final Priority priority) {
++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
++ * for single load.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ * @param types The regionfile type(s) to load.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
++ */
++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Consumer onComplete, final boolean intendingToBlock,
++ final RegionFileType... types) {
++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
++ * for single load.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ * @param types The regionfile type(s) to load.
++ * @param priority The minimum priority to load the data at.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
++ */
++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Consumer onComplete, final boolean intendingToBlock,
++ final Priority priority, final RegionFileType... types) {
++ if (types == null) {
++ throw new NullPointerException("Types cannot be null");
++ }
++ if (types.length == 0) {
++ throw new IllegalArgumentException("Types cannot be empty");
++ }
++
++ final RegionFileData ret = new RegionFileData();
++
++ final Cancellable[] reads = new CancellableRead[types.length];
++ final AtomicInteger completions = new AtomicInteger();
++ final int expectedCompletions = types.length;
++
++ for (int i = 0; i < expectedCompletions; ++i) {
++ final RegionFileType type = types[i];
++ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type,
++ (final CompoundTag data, final Throwable throwable) -> {
++ if (throwable != null) {
++ ret.setThrowable(type, throwable);
++ } else {
++ ret.setData(type, data);
++ }
++
++ if (completions.incrementAndGet() == expectedCompletions) {
++ onComplete.accept(ret);
++ }
++ }, intendingToBlock, priority);
++ }
++
++ return new CancellableReads(reads);
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
++ * {@code onComplete}.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
++ */
++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
++ final RegionFileType type, final BiConsumer onComplete,
++ final boolean intendingToBlock) {
++ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
++ }
++
++ /**
++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
++ * {@code onComplete}.
++ *
++ * Impl notes:
++ *
++ *
++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
++ * data is undefined behaviour, and can cause deadlock.
++ *
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param onComplete Consumer to execute once this task has completed
++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
++ * of this call.
++ * @param priority Minimum priority to load the data at.
++ *
++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
++ *
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
++ */
++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
++ final RegionFileType type, final BiConsumer onComplete,
++ final boolean intendingToBlock, final 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 onComplete,
++ final boolean intendingToBlock, final Priority priority) {
++ final ChunkDataController taskController = getControllerFor(world, type);
++
++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
++
++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final BiLong1Function compute = (final long keyInMap, final ChunkDataTask running) -> {
++ if (running == null) {
++ // not scheduled
++
++ // set up task
++ final ChunkDataTask newTask = new ChunkDataTask(
++ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority
++ );
++ newTask.inProgressRead = new InProgressRead();
++ newTask.inProgressRead.addToAsyncWaiters(onComplete);
++
++ callbackInfo.tasksNeedsScheduling = true;
++ return newTask;
++ }
++
++ final CompoundTag pendingWrite = running.inProgressWrite;
++
++ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) {
++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations
++ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) {
++ callbackInfo.data = running.inProgressRead.value;
++ callbackInfo.throwable = running.inProgressRead.throwable;
++ callbackInfo.completeNow = true;
++ }
++ return running;
++ }
++
++ // at this stage we have to use the in progress write's data to avoid an order issue
++ callbackInfo.data = pendingWrite;
++ callbackInfo.throwable = null;
++ callbackInfo.completeNow = true;
++ return running;
++ };
++
++ final ChunkDataTask ret = taskController.tasks.compute(key, compute);
++
++ // needs to be scheduled
++ if (callbackInfo.tasksNeedsScheduling) {
++ ret.prioritisedTask.queue();
++ } else if (callbackInfo.completeNow) {
++ try {
++ onComplete.accept(callbackInfo.data == 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);
++ }
++ } else {
++ // we're waiting on a task we didn't schedule, so raise its priority to what we want
++ ret.prioritisedTask.raisePriority(priority);
++ }
++
++ return new CancellableRead(onComplete, ret);
++ }
++
++ /**
++ * Schedules a load task to be executed asynchronously, and blocks on that task.
++ *
++ * @param world Chunk's world
++ * @param chunkX Chunk's x coordinate
++ * @param chunkZ Chunk's z coordinate
++ * @param type Regionfile type
++ * @param priority Minimum priority to load the data at.
++ *
++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk.
++ *
++ * @throws IOException If the load fails for any reason
++ */
++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
++ final Priority priority) throws IOException {
++ final CompletableFuture ret = new CompletableFuture<>();
++
++ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
++ if (thr != null) {
++ ret.completeExceptionally(thr);
++ } else {
++ ret.complete(compound);
++ }
++ }, true, priority);
++
++ try {
++ return ret.join();
++ } catch (final CompletionException ex) {
++ throw new IOException(ex);
++ }
++ }
++
++ private static final class ImmediateCallbackCompletion {
++
++ public CompoundTag data;
++ public Throwable throwable;
++ public boolean completeNow;
++ public boolean tasksNeedsScheduling;
++
++ }
++
++ private static final class CancellableRead implements Cancellable {
++
++ private BiConsumer callback;
++ private ChunkDataTask task;
++
++ CancellableRead(final BiConsumer callback, final ChunkDataTask task) {
++ this.callback = callback;
++ this.task = task;
++ }
++
++ @Override
++ public boolean cancel() {
++ final BiConsumer callback = this.callback;
++ final ChunkDataTask task = this.task;
++
++ if (callback == null || task == null) {
++ return false;
++ }
++
++ this.callback = null;
++ this.task = null;
++
++ 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.cancel(callback);
++ }
++ }
++
++ private static final class CancellableReads implements Cancellable {
++
++ private Cancellable[] reads;
++
++ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
++
++ CancellableReads(final Cancellable[] reads) {
++ this.reads = reads;
++ }
++
++ @Override
++ public boolean cancel() {
++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null);
++
++ if (reads == null) {
++ return false;
++ }
++
++ boolean ret = false;
++
++ for (final Cancellable read : reads) {
++ ret |= read.cancel();
++ }
++
++ return ret;
++ }
++ }
++
++ private static final class InProgressRead {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
++
++ private CompoundTag value;
++ private Throwable throwable;
++ private final MultiThreadedQueue> callbacks = new MultiThreadedQueue<>();
++
++ public boolean hasNoWaiters() {
++ return this.callbacks.isEmpty();
++ }
++
++ public boolean addToAsyncWaiters(final BiConsumer callback) {
++ return this.callbacks.add(callback);
++ }
++
++ public boolean cancel(final BiConsumer callback) {
++ return this.callbacks.remove(callback);
++ }
++
++ public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) {
++ this.value = value;
++ this.throwable = throwable;
++
++ BiConsumer consumer;
++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
++ try {
++ 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);
++ }
++ }
++ }
++ }
++
++ public static abstract class ChunkDataController {
++
++ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
++ private final ConcurrentLong2ReferenceChainedHashTable tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f);
++
++ public final RegionFileType type;
++
++ public ChunkDataController(final RegionFileType type) {
++ this.type = type;
++ }
++
++ public abstract RegionFileStorage getCache();
++
++ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException;
++
++ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException;
++
++ public boolean hasTasks() {
++ return !this.tasks.isEmpty();
++ }
++
++ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) {
++ return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ);
++ }
++
++ public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) {
++ final RegionFileStorage cache = this.getCache();
++ final RegionFile regionFile;
++ synchronized (cache) {
++ try {
++ 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);
++ }
++
++ return function.apply(regionFile);
++ }
++ }
++
++ public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) {
++ final RegionFileStorage cache = this.getCache();
++ final RegionFile regionFile;
++
++ synchronized (cache) {
++ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ);
++
++ return function.apply(regionFile);
++ }
++ }
++ }
++
++ private static final class ChunkDataTask implements Runnable {
++
++ private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class);
++
++ private InProgressRead inProgressRead;
++ private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
++
++ private boolean failedWrite;
++
++ private final ServerLevel world;
++ private final int chunkX;
++ private final int chunkZ;
++ private final ChunkDataController taskController;
++
++ private final PrioritisedTask prioritisedTask;
++
++ /*
++ * IO thread will perform reads before writes for a given chunk x and z
++ *
++ * How reads/writes are scheduled:
++ *
++ * If read is scheduled while scheduling write, take no special action and just schedule write
++ * If read is scheduled while scheduling read and no write is scheduled, chain the read task
++ *
++ *
++ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled)
++ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data
++ *
++ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
++ * it fails to properly propagate write failures thanks to writes overwriting each other
++ */
++
++ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController,
++ final PrioritisedExecutor executor, final Priority priority) {
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.taskController = taskController;
++ this.prioritisedTask = executor.createTask(this, priority);
++ }
++
++ @Override
++ public String toString() {
++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ +
++ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode();
++ }
++
++ @Override
++ public void run() {
++ 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.hasNoWaiters()) {
++ // cancelled read? go to task controller to confirm
++ 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!");
++ }
++ if (valueInMap != ChunkDataTask.this) {
++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
++ }
++
++ if (!read.hasNoWaiters()) {
++ return valueInMap;
++ } else {
++ canRead[0] = false;
++ }
++
++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
++ });
++
++ if (inMap == null) {
++ // read is cancelled - and no write pending, so we're done
++ return;
++ }
++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries -
++ // the readers will just use the in progress write, so the value in canRead is good to use without
++ // further synchronisation.
++ }
++
++ if (canRead[0]) {
++ CompoundTag compound = null;
++ Throwable throwable = null;
++
++ try {
++ compound = this.taskController.readData(this.chunkX, this.chunkZ);
++ } catch (final Throwable thr) {
++ throwable = thr;
++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
++ }
++ read.complete(this, compound, throwable);
++ }
++ }
++
++ CompoundTag write = this.inProgressWrite;
++
++ if (write == NOTHING_TO_WRITE) {
++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
++ if (valueInMap == null) {
++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
++ }
++ if (valueInMap != ChunkDataTask.this) {
++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
++ }
++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
++ });
++
++ if (inMap == null) {
++ return; // set the task value to null, indicating we're done
++ } // else: inProgressWrite changed, so now we have something to write
++ }
++
++ for (;;) {
++ write = this.inProgressWrite;
++ final CompoundTag dataWritten = write;
++
++ boolean failedWrite = false;
++
++ try {
++ this.taskController.writeData(this.chunkX, this.chunkZ, write);
++ } catch (final 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 '" + 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);
++ }
++ }
++
++ final boolean finalFailWrite = failedWrite;
++ final boolean[] done = new boolean[] { false };
++
++ this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
++ if (valueInMap == null) {
++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
++ }
++ if (valueInMap != ChunkDataTask.this) {
++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
++ }
++ if (valueInMap.inProgressWrite == dataWritten) {
++ valueInMap.failedWrite = finalFailWrite;
++ done[0] = true;
++ // keep the data in map if we failed the write so we can try to prevent data loss
++ return finalFailWrite ? valueInMap : null;
++ }
++ // different data than expected, means we need to retry write
++ return valueInMap;
++ });
++
++ if (done[0]) {
++ return;
++ }
++
++ // fetch & write new data
++ continue;
++ }
++ }
++ }
++}
+diff --git a/src/main/java/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 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..fdd189ef056187941d43809c5d61cab717aecf60
+--- /dev/null
++++ 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.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.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> onLoad);
++
++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
++ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
++ final Consumer> onLoad);
++
++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
++ final PrioritisedExecutor.Priority priority,
++ final Consumer> 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> 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 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.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.Arrays;
++import java.util.Iterator;
++import java.util.List;
++import java.util.function.Predicate;
++
++public final class ChunkEntitySlices {
++
++ public final int minSection;
++ public final int maxSection;
++ public final int chunkX;
++ public final int chunkZ;
++ public final Level world;
++
++ private final EntityCollectionBySection allEntities;
++ private final EntityCollectionBySection hardCollidingEntities;
++ private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass;
++ private final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByType;
++ private final EntityList entities = new EntityList();
++
++ public FullChunkStatus status;
++
++ private boolean isTransient;
++
++ public boolean isTransient() {
++ return this.isTransient;
++ }
++
++ 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 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;
++ }
++ 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 static CompoundTag saveEntityChunk(final List entities, final ChunkPos chunkPos, final ServerLevel world) {
++ return saveEntityChunk0(entities, chunkPos, world, false);
++ }
++
++ public static CompoundTag saveEntityChunk0(final List entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) {
++ if (!force && entities.isEmpty()) {
++ return null;
++ }
++
++ 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 CompoundTag save() {
++ final int len = this.entities.size();
++ if (len == 0) {
++ return null;
++ }
++
++ final Entity[] rawData = this.entities.getRawData();
++ final List collectedEntities = new ArrayList<>(len);
++ for (int i = 0; i < len; ++i) {
++ final Entity entity = rawData[i];
++ if (entity.shouldBeSaved()) {
++ collectedEntities.add(entity);
++ }
++ }
++
++ if (collectedEntities.isEmpty()) {
++ return null;
++ }
++
++ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world);
++ }
++
++ // returns true if this chunk has transient entities remaining
++ public boolean unload() {
++ final int len = this.entities.size();
++ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
++
++ for (int i = 0; i < len; ++i) {
++ final Entity entity = collectedEntities[i];
++ if (entity.isRemoved()) {
++ // removed by us below
++ continue;
++ }
++ if (entity.shouldBeSaved()) {
++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
++ if (entity.isVehicle()) {
++ // we cannot assume that these entities are contained within this chunk, because entities can
++ // desync - so we need to remove them all
++ for (final Entity passenger : entity.getIndirectPassengers()) {
++ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
++ }
++ }
++ }
++ }
++
++ return this.entities.size() != 0;
++ }
++
++ // Paper start
++ public org.bukkit.entity.Entity[] getChunkEntities() {
++ List ret = new java.util.ArrayList<>();
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ if (entity == null) {
++ continue;
++ }
++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
++ if (bukkit != null && bukkit.isValid()) {
++ ret.add(bukkit);
++ }
++ }
++
++ return ret.toArray(new org.bukkit.entity.Entity[0]);
++ }
++ // Paper end
++
++ private List getAllEntities() {
++ final int len = this.entities.size();
++ if (len == 0) {
++ return new ArrayList<>();
++ }
++
++ final Entity[] rawData = this.entities.getRawData();
++ final List collectedEntities = new ArrayList<>(len);
++ for (int i = 0; i < len; ++i) {
++ collectedEntities.add(rawData[i]);
++ }
++
++ return collectedEntities;
++ }
++
++ public boolean isEmpty() {
++ return this.entities.size() == 0;
++ }
++
++ public void mergeInto(final ChunkEntitySlices slices) {
++ final Entity[] entities = this.entities.getRawData();
++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
++ final Entity entity = entities[i];
++ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY());
++ }
++ }
++
++ 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, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next();
++
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().addEntity(entity, sectionIndex);
++ }
++ }
++
++ 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 true;
++ }
++
++ 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, EntityCollectionBySection>> iterator =
++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next();
++
++ if (entry.getKey().isInstance(entity)) {
++ entry.getValue().removeEntity(entity, sectionIndex);
++ }
++ }
++
++ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
++ byType.removeEntity(entity, sectionIndex);
++
++ return true;
++ }
++
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ this.hardCollidingEntities.getEntities(except, box, into, predicate);
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
++ }
++
++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate super Entity> predicate) {
++ this.allEntities.getEntities(except, box, into, predicate);
++ }
++
++
++ public boolean getEntities(final Entity except, final AABB box, final List 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 into, final Predicate super Entity> predicate,
++ final int maxCount) {
++ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount);
++ }
++
++ public 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 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 sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
++ if (sectionEntities == null) {
++ continue;
++ }
++
++ final Entity[] storage = sectionEntities.storage;
++
++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (clazz.isInstance(entity)) {
++ ret.addEntity(entity, sectionIndex);
++ }
++ }
++ }
++
++ return ret;
++ }
++
++ public void getEntities(final Class 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);
++ }
++ }
++
++ public 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);
++ }
++ }
++
++ private static final class BasicEntityList {
++
++ private static final Entity[] EMPTY = new Entity[0];
++ private static final int DEFAULT_CAPACITY = 4;
++
++ 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;
++ }
++ }
++
++ public int indexOf(final E entity) {
++ final E[] storage = this.storage;
++
++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
++ if (storage[i] == entity) {
++ return i;
++ }
++ }
++
++ return -1;
++ }
++
++ public boolean remove(final E entity) {
++ final int idx = this.indexOf(entity);
++ if (idx == -1) {
++ return false;
++ }
++
++ final int size = --this.size;
++ final E[] storage = this.storage;
++ if (idx != size) {
++ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
++ }
++
++ storage[size] = null;
++
++ return true;
++ }
++
++ public boolean has(final E entity) {
++ return this.indexOf(entity) != -1;
++ }
++ }
++
++ private static final class EntityCollectionBySection {
++
++ private final ChunkEntitySlices slices;
++ private final BasicEntityList[] 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 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 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 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(entity)) {
++ continue;
++ }
++
++ into.add(entity);
++ }
++ }
++ }
++
++ public boolean getEntitiesLimited(final Entity except, final AABB box, final List 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(entity)) {
++ continue;
++ }
++
++ into.add(entity);
++ if (into.size() >= maxCount) {
++ return true;
++ }
++ }
++ }
++
++ return false;
++ }
++
++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
++ if (part == except || !part.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(part)) {
++ continue;
++ }
++
++ into.add(part);
++ }
++ }
++ }
++ }
++ }
++
++ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ 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 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ } // else: continue to test the ender dragon parts
++
++ if (entity instanceof EnderDragon) {
++ for (final EnderDragonPart part : ((EnderDragon)entity).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 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[] entitiesBySection = this.entitiesBySection;
++
++ for (int section = min; section <= max; ++section) {
++ final BasicEntityList list = entitiesBySection[section - minSection];
++
++ if (list == null) {
++ continue;
++ }
++
++ final Entity[] storage = list.storage;
++
++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
++ final Entity entity = storage[i];
++
++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
++ continue;
++ }
++
++ if (predicate == null || predicate.test(entity)) {
++ into.add(entity);
++ 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;
++
++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 {
++
++ 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 regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
++
++ protected final int minSection; // inclusive
++ protected final int maxSection; // inclusive
++ protected final LevelCallback worldCallback;
++
++ protected final ConcurrentLong2ReferenceChainedHashTable entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
++ protected final ConcurrentHashMap entityByUUID = new ConcurrentHashMap<>();
++ protected final EntityList accessibleEntities = new EntityList();
++
++ public EntityLookup(final Level world, final LevelCallback 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;
++ }
++ 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 implements Iterable {
++
++ private final T[] array;
++ private final int off;
++ private final int length;
++
++ public ArrayIterable(final T[] array, final int off, final int length) {
++ this.array = array;
++ this.off = off;
++ this.length = length;
++ if (length > array.length) {
++ throw new IllegalArgumentException("Length must be no greater-than the array length");
++ }
++ }
++
++ @Override
++ public Iterator iterator() {
++ return new ArrayIterator<>(this.array, this.off, this.length);
++ }
++
++ protected static final class ArrayIterator implements Iterator {
++
++ private final T[] array;
++ private int off;
++ private final int length;
++
++ public ArrayIterator(final T[] array, final int off, final int length) {
++ this.array = array;
++ this.off = off;
++ this.length = length;
++ }
++
++ @Override
++ public boolean hasNext() {
++ return this.off < this.length;
++ }
++
++ @Override
++ public T next() {
++ if (this.off >= this.length) {
++ throw new NoSuchElementException();
++ }
++ return this.array[this.off++];
++ }
++
++ @Override
++ public void remove() {
++ throw new UnsupportedOperationException();
++ }
++ }
++ }
++
++ @Override
++ public Iterable getAll() {
++ synchronized (this.accessibleEntities) {
++ final int len = this.accessibleEntities.size();
++ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class);
++
++ Objects.checkFromToIndex(0, len, cpy.length);
++
++ return new ArrayIterable<>(cpy, 0, len);
++ }
++ }
++
++ public int getEntityCount() {
++ synchronized (this.accessibleEntities) {
++ return this.accessibleEntities.size();
++ }
++ }
++
++ public Entity[] getAllCopy() {
++ synchronized (this.accessibleEntities) {
++ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
++ }
++ }
++
++ @Override
++ public void get(final EntityTypeTest filter, final AbortableIterationConsumer action) {
++ for (final Iterator 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;
++ }
++ }
++ }
++
++ @Override
++ public void get(final AABB box, final Consumer action) {
++ List entities = new ArrayList<>();
++ this.getEntitiesWithoutDragonParts(null, box, entities, null);
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ action.accept(entities.get(i));
++ }
++ }
++
++ @Override
++ public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) {
++ List entities = new ArrayList<>();
++ this.getEntitiesWithoutDragonParts(null, box, entities, null);
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ final U casted = filter.tryCast(entities.get(i));
++ if (casted != null && action.accept(casted).shouldAbort()) {
++ break;
++ }
++ }
++ }
++
++ 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;
++ }
++
++ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
++
++ if (entityStatusUpdateBefore) {
++ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
++ return;
++ }
++
++ try {
++ final Boolean ticketBlockBefore = this.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);
++ }
++ }
++ }
++
++ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
++ this.getChunk(x, z).updateStatus(newStatus, this);
++ }
++
++ public void addLegacyChunkEntities(final List entities, final ChunkPos forChunk) {
++ this.addEntityChunk(entities, forChunk, true);
++ }
++
++ public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) {
++ this.addEntityChunk(entities, forChunk, true);
++ }
++
++ public void addWorldGenChunkEntities(final List 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 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;
++ }
++
++ 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 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);
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List 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.getEntities(except, box, into, predicate);
++ }
++ }
++ }
++ }
++ }
++
++ public void getHardCollidingEntities(final Entity except, final AABB box, final List 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 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);
++ }
++ }
++ }
++ }
++ }
++
++ public 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 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.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) {
++ return;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ public void getEntities(final Entity except, final AABB box, final List 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;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ public 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 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) {}
++ }
++}
+\ 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..fc4ea13aa4a21bd3d3f9377418a24b904868c401
+--- /dev/null
++++ 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;
++
++public final class ClientEntityLookup extends EntityLookup {
++
++ private final LongOpenHashSet tickingChunks = new LongOpenHashSet();
++
++ public ClientEntityLookup(final Level world, final LevelCallback 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 {
++
++ @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 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 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 net.minecraft.SharedConstants;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.ai.village.poi.PoiManager;
++import net.minecraft.world.entity.ai.village.poi.PoiSection;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.util.Optional;
++
++public final class PoiChunk {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class);
++
++ public final ServerLevel world;
++ public final int chunkX;
++ public final int chunkZ;
++ public final int minSection;
++ public final int maxSection;
++
++ private final PoiSection[] sections;
++
++ private boolean isDirty;
++ private boolean loaded;
++
++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) {
++ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]);
++ }
++
++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) {
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.minSection = minSection;
++ this.maxSection = maxSection;
++ this.sections = sections;
++ if (this.sections.length != (maxSection - minSection + 1)) {
++ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length);
++ }
++ }
++
++ public void load() {
++ 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;
++ ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this);
++ }
++
++ public boolean isLoaded() {
++ return this.loaded;
++ }
++
++ public boolean isEmpty() {
++ for (final PoiSection section : this.sections) {
++ if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
++ return false;
++ }
++ }
++
++ return true;
++ }
++
++ public PoiSection getOrCreateSection(final int chunkY) {
++ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
++ final int idx = chunkY - this.minSection;
++ final PoiSection ret = this.sections[idx];
++ if (ret != null) {
++ return ret;
++ }
++
++ final PoiManager poiManager = this.world.getPoiManager();
++ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ);
++
++ return this.sections[idx] = new PoiSection(() -> {
++ poiManager.setDirty(key);
++ });
++ }
++ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]");
++ }
++
++ public PoiSection getSection(final int chunkY) {
++ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
++ return this.sections[chunkY - this.minSection];
++ }
++ return null;
++ }
++
++ public Optional getSectionForVanilla(final int chunkY) {
++ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
++ final PoiSection ret = this.sections[chunkY - this.minSection];
++ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional();
++ }
++ return Optional.empty();
++ }
++
++ public boolean isDirty() {
++ return this.isDirty;
++ }
++
++ public void setDirty(final boolean dirty) {
++ this.isDirty = dirty;
++ }
++
++ // returns null if empty
++ public CompoundTag save() {
++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess());
++
++ final CompoundTag ret = new CompoundTag();
++ final CompoundTag sections = new CompoundTag();
++ ret.put("Sections", sections);
++
++ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
++
++ final ServerLevel world = this.world;
++ final PoiManager poiManager = world.getPoiManager();
++ final int chunkX = this.chunkX;
++ final int chunkZ = this.chunkZ;
++
++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
++ final PoiSection section = this.sections[sectionY - this.minSection];
++ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
++ continue;
++ }
++
++ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
++ // codecs are honestly such a fucking disaster. What the fuck is this trash?
++ final Codec codec = PoiSection.codec(() -> {
++ poiManager.setDirty(key);
++ });
++
++ final DataResult serializedResult = codec.encodeStart(registryOps, section);
++ final int finalSectionY = sectionY;
++ final Tag serialized = serializedResult.resultOrPartial((final String 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
++ continue;
++ }
++
++ sections.put(Integer.toString(sectionY), serialized);
++ }
++
++ return sections.isEmpty() ? null : ret;
++ }
++
++ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) {
++ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world));
++ ret.loaded = true;
++ return ret;
++ }
++
++ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) {
++ final PoiChunk ret = empty(world, chunkX, chunkZ);
++
++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess());
++
++ final CompoundTag sections = data.getCompound("Sections");
++
++ if (sections.isEmpty()) {
++ // nothing to parse
++ return ret;
++ }
++
++ final PoiManager poiManager = world.getPoiManager();
++
++ boolean readAnything = false;
++
++ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) {
++ final String key = Integer.toString(sectionY);
++ if (!sections.contains(key)) {
++ continue;
++ }
++
++ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
++ // codecs are honestly such a fucking disaster. What the fuck is this trash?
++ final Codec codec = PoiSection.codec(() -> {
++ poiManager.setDirty(coordinateKey);
++ });
++
++ final CompoundTag section = sections.getCompound(key);
++ final DataResult deserializeResult = codec.parse(registryOps, section);
++ final int finalSectionY = sectionY;
++ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> {
++ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
++ }).orElse(null);
++
++ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) {
++ // completely empty, no point in storing this
++ continue;
++ }
++
++ readAnything = true;
++ ret.sections[sectionY - ret.minSection] = deserialized;
++ }
++
++ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load
++
++ return ret;
++ }
++}
+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..003a857e70ead858e8437e3c1bfaf22f4daba0df
+--- /dev/null
++++ 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 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.LevelChunk;
++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 RegionizedPlayerChunkLoader {
++
++ public static final TicketType PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
++ public static final TicketType PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
++
++ public static final int MIN_VIEW_DISTANCE = 2;
++ public static final int MAX_VIEW_DISTANCE = 32;
++
++ 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 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);
++ });
++ }
++ }
++
++ 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);
++ }
++ }
++
++ 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;
++ }
++
++ 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;
++ }
++
++ 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;
++ }
++
++ 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;
++ }
++
++ 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();
++ }
++
++ public void updatePlayer(final ServerPlayer player) {
++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
++ if (loader != null) {
++ loader.update();
++ }
++ }
++
++ 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);
++ }
++
++ 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;
++ }
++
++ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ 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;
++ }
++
++ 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);
++ }
++ }
++
++ 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> 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);
++ }
++
++ // 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 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 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 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 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;
++ }
++
++ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
++
++ public List retrieveForAllRegions() {
++ final List ret = new ArrayList<>();
++
++ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry 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()));
++ }
++
++ 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) {
++ 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;
++ }
++ }
++}
+\ 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..a7e7569b9d4160e7d92422ca5c1cce7f46b78f2e
+--- /dev/null
++++ 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.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 it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.longs.Long2ByteMap;
++import it.unimi.dsi.fastutil.longs.Long2IntMap;
++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
++import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayList;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ChunkLevel;
++import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.Ticket;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.util.SortedArraySet;
++import net.minecraft.util.Unit;
++import net.minecraft.world.level.ChunkPos;
++import 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.Iterator;
++import java.util.List;
++import java.util.Objects;
++import java.util.PrimitiveIterator;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicReference;
++import java.util.concurrent.locks.LockSupport;
++import java.util.function.Predicate;
++
++public final class ChunkHolderManager {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class);
++
++ 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 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 ConcurrentLong2ReferenceChainedHashTable>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>();
++ private final ConcurrentLong2ReferenceChainedHashTable sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>();
++ final ChunkUnloadQueue unloadQueue;
++
++ private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
++ private final ServerLevel world;
++ private final ChunkTaskScheduler taskScheduler;
++ private long currentTick;
++
++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
++ if (c1 == c2) {
++ return 0;
++ }
++
++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
++
++ if (saveTickCompare != 0) {
++ return saveTickCompare;
++ }
++
++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
++
++ if (coord1 == coord2) {
++ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
++ }
++
++ return Long.compare(coord1, coord2);
++ });
++
++ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
++ this.world = world;
++ this.taskScheduler = taskScheduler;
++ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift());
++ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
++ }
++
++ public boolean processTicketUpdates(final int posX, final int posZ) {
++ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
++ final int ticketMask = (1 << ticketShift) - 1;
++ final List scheduledTasks = new ArrayList<>();
++ final List 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 boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List scheduledTasks,
++ final List changedFullStatus) {
++ return this.ticketLevelPropagator.performUpdate(
++ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
++ );
++ }
++
++ public List getOldChunkHolders() {
++ final List ret = new ArrayList<>(this.chunkHolders.size() + 1);
++ for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
++ ret.add(iterator.next().vanillaChunkHolder);
++ }
++ return ret;
++ }
++
++ public List getChunkHolders() {
++ final List ret = new ArrayList<>(this.chunkHolders.size() + 1);
++ for (final Iterator iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
++ ret.add(iterator.next());
++ }
++ return ret;
++ }
++
++ public int size() {
++ return this.chunkHolders.size();
++ }
++
++ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks
++ public Iterable getOldChunkHoldersIterable() {
++ return new Iterable() {
++ @Override
++ public Iterator iterator() {
++ final Iterator iterator = ChunkHolderManager.this.chunkHolders.valueIterator();
++ return new Iterator() {
++ @Override
++ public boolean hasNext() {
++ return iterator.hasNext();
++ }
++
++ @Override
++ public ChunkHolder next() {
++ return iterator.next().vanillaChunkHolder;
++ }
++ };
++ }
++ };
++ }
++
++ public void close(final boolean save, final boolean halt) {
++ io.papermc.paper.util.TickThread.ensureTickThread("Closing world off-main");
++ if (halt) {
++ 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 '" + WorldUtil.getWorldName(this.world) + "'");
++ } else {
++ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'");
++ }
++ }
++
++ if (save) {
++ this.saveAllChunks(true, true, true);
++ }
++
++ 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
++ 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 = this.currentTick;
++ this.autoSaveQueue.add(holder);
++ }
++ }
++
++ public void autoSave() {
++ final List reschedule = new ArrayList<>();
++ 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) {
++ break;
++ }
++
++ this.autoSaveQueue.remove(holder);
++
++ holder.lastAutoSave = currentTick;
++ if (holder.save(false) != null) {
++ ++autoSaved;
++ }
++
++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
++ reschedule.add(holder);
++ }
++ }
++
++ for (final NewChunkHolder holder : reschedule) {
++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
++ this.autoSaveQueue.add(holder);
++ }
++ }
++ }
++
++ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
++ final List holders = this.getChunkHolders();
++
++ if (logProgress) {
++ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
++ }
++
++ final DecimalFormat format = new DecimalFormat("#0.00");
++
++ int saved = 0;
++
++ long start = System.nanoTime();
++ long lastLog = start;
++ boolean needsFlush = false;
++ final int flushInterval = 50;
++
++ int savedChunk = 0;
++ int savedEntity = 0;
++ int savedPoi = 0;
++
++ for (int i = 0, len = holders.size(); i < len; ++i) {
++ final NewChunkHolder holder = holders.get(i);
++ try {
++ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
++ if (saveStat != null) {
++ ++saved;
++ needsFlush = flush;
++ if (saveStat.savedChunk()) {
++ ++savedChunk;
++ }
++ if (saveStat.savedEntityChunk()) {
++ ++savedEntity;
++ }
++ if (saveStat.savedPoiChunk()) {
++ ++savedPoi;
++ }
++ }
++ } catch (final Throwable 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;
++ RegionFileIOThread.partialFlush(flushInterval / 2);
++ }
++ if (logProgress) {
++ final long currTime = System.nanoTime();
++ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) {
++ lastLog = currTime;
++ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'");
++ }
++ }
++ }
++ if (flush) {
++ RegionFileIOThread.flush();
++ 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 '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s");
++ }
++ }
++
++ 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
++ for (final Iterator iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) {
++ final Long2ByteMap.Entry entry = iterator.next();
++ final long key = entry.getLongKey();
++ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue());
++
++ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
++ if (current == null && newLevel > MAX_TICKET_LEVEL) {
++ // not loaded and it shouldn't be loaded!
++ iterator.remove();
++ continue;
++ }
++
++ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel();
++ if (currentLevel == newLevel) {
++ // nothing to do
++ iterator.remove();
++ continue;
++ }
++
++ if (current == null) {
++ // must create
++ current = ChunkHolderManager.this.createChunkHolder(key);
++ ChunkHolderManager.this.chunkHolders.put(key, current);
++ current.updateTicketLevel(newLevel);
++ } else {
++ current.updateTicketLevel(newLevel);
++ }
++ }
++ }
++
++ @Override
++ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks,
++ final List changedFullStatus) {
++ final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get();
++ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks);
++ try {
++ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) {
++ final long key = iterator.nextLong();
++ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
++
++ if (current == null) {
++ throw new IllegalStateException("Expected chunk holder to be created");
++ }
++
++ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus);
++ }
++ } finally {
++ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev);
++ }
++ }
++ };
++ // function for converting between ticket levels and propagator levels and vice versa
++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects
++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator
++ // and the levels we get out of the propagator
++
++ public static int convertBetweenTicketLevels(final int level) {
++ return ChunkLevel.MAX_LEVEL - level + 1;
++ }
++
++ public String getTicketDebugString(final long coordinate) {
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
++ try {
++ final SortedArraySet> tickets = this.tickets.get(coordinate);
++
++ return tickets != null ? tickets.first().toString() : "no_ticket";
++ } finally {
++ if (ticketLock != null) {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++ }
++
++ public Long2ObjectOpenHashMap>> getTicketsCopy() {
++ final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>();
++ final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>();
++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
++ final long coord = iterator.nextLong();
++ sections.computeIfAbsent(
++ CoordinateUtils.getChunkKey(
++ CoordinateUtils.getChunkX(coord) >> sectionShift,
++ CoordinateUtils.getChunkZ(coord) >> sectionShift
++ ),
++ (final long keyInMap) -> {
++ return new LongArrayList();
++ }
++ ).add(coord);
++ }
++
++ for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator();
++ iterator.hasNext();) {
++ final Long2ObjectMap.Entry entry = iterator.next();
++ final long sectionKey = entry.getLongKey();
++ final LongArrayList coordinates = entry.getValue();
++
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
++ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
++ );
++ try {
++ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
++ final long coord = iterator2.nextLong();
++ final SortedArraySet> tickets = this.tickets.get(coord);
++ if (tickets == null) {
++ // removed before we acquired lock
++ continue;
++ }
++ ret.put(coord, ((ChunkSystemSortedArraySet>)tickets).moonrise$copy());
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++
++ return ret;
++ }
++
++ // Paper start
++ public Collection getPluginChunkTickets(int x, int z) {
++ com.google.common.collect.ImmutableList.Builder ret;
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z);
++ try {
++ final long coordinate = CoordinateUtils.getChunkKey(x, z);
++ final SortedArraySet> tickets = this.tickets.get(coordinate);
++
++ if (tickets == null) {
++ return java.util.Collections.emptyList();
++ }
++
++ ret = com.google.common.collect.ImmutableList.builder();
++ for (Ticket> ticket : tickets) {
++ if (ticket.getType() == TicketType.PLUGIN_TICKET) {
++ ret.add((org.bukkit.plugin.Plugin)ticket.key);
++ }
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++
++ return ret.build();
++ }
++ // Paper end
++
++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
++ if (ticketLevel > ChunkLevel.MAX_LEVEL) {
++ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
++ } else {
++ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel));
++ }
++ }
++
++ private static int getTicketLevelAt(SortedArraySet> tickets) {
++ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1;
++ }
++
++ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level,
++ final T identifier) {
++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
++ }
++
++ public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level,
++ final T identifier) {
++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
++ }
++
++ private void addExpireCount(final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
++ final long sectionKey = CoordinateUtils.getChunkKey(
++ chunkX >> sectionShift,
++ chunkZ >> sectionShift
++ );
++
++ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> {
++ return new Long2IntOpenHashMap();
++ }).addTo(chunkKey, 1);
++ }
++
++ private void removeExpireCount(final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ 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);
++
++ if (prevCount == 1) {
++ removeCounts.remove(chunkKey);
++ if (removeCounts.isEmpty()) {
++ this.sectionToChunkToExpireCount.remove(sectionKey);
++ }
++ }
++ }
++
++ // supposed to return true if the ticket was added and did not replace another
++ // but, we always return false if the ticket cannot be added
++ public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) {
++ return this.addTicketAtLevel(type, chunk, level, identifier, true);
++ }
++
++ boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) {
++ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout;
++ if (level > MAX_TICKET_LEVEL) {
++ return false;
++ }
++
++ final int chunkX = CoordinateUtils.getChunkX(chunk);
++ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
++ final Ticket ticket = new Ticket<>(type, level, identifier);
++ ((ChunkSystemTicket)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
++
++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
++ try {
++ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
++ return SortedArraySet.create(4);
++ });
++
++ final int levelBefore = getTicketLevelAt(ticketsAtChunk);
++ final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket);
++ final int levelAfter = getTicketLevelAt(ticketsAtChunk);
++
++ if (current != ticket) {
++ final long oldRemoveDelay = ((ChunkSystemTicket)(Object)current).moonrise$getRemoveDelay();
++ if (removeDelay != oldRemoveDelay) {
++ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) {
++ this.removeExpireCount(chunkX, chunkZ);
++ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) {
++ // since old != new, we have that NO_TIMEOUT_MARKER != new
++ this.addExpireCount(chunkX, chunkZ);
++ }
++ }
++ } else {
++ if (removeDelay != NO_TIMEOUT_MARKER) {
++ this.addExpireCount(chunkX, chunkZ);
++ }
++ }
++
++ if (levelBefore != levelAfter) {
++ this.updateTicketLevel(chunk, levelAfter);
++ }
++
++ return current == ticket;
++ } finally {
++ if (ticketLock != null) {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++ }
++
++ public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) {
++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
++ }
++
++ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) {
++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
++ }
++
++ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) {
++ return this.removeTicketAtLevel(type, chunk, level, identifier, true);
++ }
++
++ boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) {
++ if (level > MAX_TICKET_LEVEL) {
++ return false;
++ }
++
++ final int chunkX = CoordinateUtils.getChunkX(chunk);
++ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
++ final Ticket probe = new Ticket<>(type, level, identifier);
++
++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
++ try {
++ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk);
++ if (ticketsAtChunk == null) {
++ return false;
++ }
++
++ final int oldLevel = getTicketLevelAt(ticketsAtChunk);
++ final Ticket ticket = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$removeAndGet(probe);
++
++ if (ticket == null) {
++ return false;
++ }
++
++ final int newLevel = getTicketLevelAt(ticketsAtChunk);
++ // we should not change the ticket levels while the target region may be ticking
++ if (oldLevel != newLevel) {
++ final Ticket unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk));
++ ((ChunkSystemTicket)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout));
++ if (ticketsAtChunk.add(unknownTicket)) {
++ this.addExpireCount(chunkX, chunkZ);
++ } else {
++ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
++ }
++ }
++
++ final long removeDelay = ((ChunkSystemTicket)(Object)ticket).moonrise$getRemoveDelay();
++ if (removeDelay != NO_TIMEOUT_MARKER) {
++ this.removeExpireCount(chunkX, chunkZ);
++ }
++
++ return true;
++ } finally {
++ if (ticketLock != null) {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++ }
++
++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
++ public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier,
++ final TicketType removeType, final int removeLevel, final V removeIdentifier) {
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
++ try {
++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
++ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false);
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++
++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
++ public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier,
++ final TicketType removeType, final int removeLevel, final V removeIdentifier) {
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
++ try {
++ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) {
++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
++ return true;
++ }
++ return false;
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++
++ public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) {
++ if (ticketLevel > MAX_TICKET_LEVEL) {
++ return;
++ }
++
++ final Long2ObjectOpenHashMap sections = new Long2ObjectOpenHashMap<>();
++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
++ final long coord = iterator.nextLong();
++ sections.computeIfAbsent(
++ CoordinateUtils.getChunkKey(
++ CoordinateUtils.getChunkX(coord) >> sectionShift,
++ CoordinateUtils.getChunkZ(coord) >> sectionShift
++ ),
++ (final long keyInMap) -> {
++ return new LongArrayList();
++ }
++ ).add(coord);
++ }
++
++ for (final Iterator> iterator = sections.long2ObjectEntrySet().fastIterator();
++ iterator.hasNext();) {
++ final Long2ObjectMap.Entry entry = iterator.next();
++ final long sectionKey = entry.getLongKey();
++ final LongArrayList coordinates = entry.getValue();
++
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
++ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
++ );
++ try {
++ 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);
++ }
++ }
++ }
++
++ public void tick() {
++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
++
++ final Predicate> expireNow = (final Ticket> ticket) -> {
++ long removeDelay = ((ChunkSystemTicket>)(Object)ticket).moonrise$getRemoveDelay();
++ if (removeDelay == NO_TIMEOUT_MARKER) {
++ return false;
++ }
++ --removeDelay;
++ ((ChunkSystemTicket>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
++ return removeDelay <= 0L;
++ };
++
++ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
++ final long sectionKey = iterator.nextLong();
++
++ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
++ // removed concurrently
++ continue;
++ }
++
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
++ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
++ );
++
++ try {
++ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey);
++ if (chunkToExpireCount == null) {
++ // lost to some race
++ continue;
++ }
++
++ for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) {
++ final Long2IntMap.Entry entry = iterator1.next();
++
++ final long chunkKey = entry.getLongKey();
++ final int expireCount = entry.getIntValue();
++
++ final SortedArraySet> tickets = this.tickets.get(chunkKey);
++ final int levelBefore = getTicketLevelAt(tickets);
++
++ final int sizeBefore = tickets.size();
++ tickets.removeIf(expireNow);
++ final int sizeAfter = tickets.size();
++ final int levelAfter = getTicketLevelAt(tickets);
++
++ if (tickets.isEmpty()) {
++ this.tickets.remove(chunkKey);
++ }
++ if (levelBefore != levelAfter) {
++ this.updateTicketLevel(chunkKey, levelAfter);
++ }
++
++ final int newExpireCount = expireCount - (sizeBefore - sizeAfter);
++
++ if (newExpireCount == expireCount) {
++ continue;
++ }
++
++ if (newExpireCount != 0) {
++ entry.setValue(newExpireCount);
++ } else {
++ iterator1.remove();
++ }
++ }
++
++ if (chunkToExpireCount.isEmpty()) {
++ this.sectionToChunkToExpireCount.remove(sectionKey);
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++ }
++
++ this.processTicketUpdates();
++ }
++
++ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) {
++ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ public NewChunkHolder getChunkHolder(final long position) {
++ return this.chunkHolders.get(position);
++ }
++
++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
++ if (chunkHolder != null) {
++ chunkHolder.raisePriority(priority);
++ }
++ }
++
++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
++ if (chunkHolder != null) {
++ chunkHolder.setPriority(priority);
++ }
++ }
++
++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
++ if (chunkHolder != null) {
++ chunkHolder.lowerPriority(priority);
++ }
++ }
++
++ private NewChunkHolder createChunkHolder(final long position) {
++ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
++
++ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
++
++ return ret;
++ }
++
++ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure
++ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders,
++ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate
++ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) {
++ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ private NewChunkHolder getOrCreateChunkHolder(final long position) {
++ final int chunkX = CoordinateUtils.getChunkX(position);
++ final int chunkZ = CoordinateUtils.getChunkZ(position);
++
++ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
++ throw new IllegalStateException("Must hold ticket level update lock!");
++ }
++ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
++ throw new IllegalStateException("Must hold scheduler lock!!");
++ }
++
++ // we could just acquire these locks, but...
++ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns
++
++ NewChunkHolder current = this.chunkHolders.get(position);
++ if (current != null) {
++ return current;
++ }
++
++ current = this.createChunkHolder(position);
++ this.chunkHolders.put(position, current);
++
++
++ return current;
++ }
++
++ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
++ ChunkEntitySlices ret;
++
++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
++ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
++ return ret;
++ }
++
++ final AtomicBoolean isCompleted = new AtomicBoolean();
++ final Thread waiter = Thread.currentThread();
++ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId();
++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
++ try {
++ 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(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
++ return ret;
++ }
++
++ if (!transientChunk) {
++ if (current.isEntityChunkNBTLoaded()) {
++ isCompleted.setPlain(true);
++ } else {
++ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> {
++ isCompleted.set(true);
++ LockSupport.unpark(waiter);
++ });
++ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
++
++ if (entityLoad != null) {
++ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ }
++ }
++ }
++ } finally {
++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++
++ if (loadTask != null) {
++ loadTask.schedule();
++ }
++
++ if (!transientChunk) {
++ // Note: no need to busy wait on the chunk queue, entity load will complete off-main
++ boolean interrupted = false;
++ while (!isCompleted.get()) {
++ interrupted |= Thread.interrupted();
++ LockSupport.park();
++ }
++
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++ }
++
++ // now that the entity data is loaded, we can load it into the world
++
++ ret = current.loadInEntityChunk(transientChunk);
++
++ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
++
++ return ret;
++ }
++
++ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) {
++ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ);
++ if (holder != null) {
++ final PoiChunk ret = holder.getPoiChunk();
++ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret;
++ }
++ return null;
++ }
++
++ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
++ 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) {
++ ret.load();
++ return ret;
++ }
++
++ final AtomicReference completed = new AtomicReference<>();
++ final AtomicBoolean isCompleted = new AtomicBoolean();
++ final Thread waiter = Thread.currentThread();
++ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId();
++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
++ try {
++ 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 (null == (ret = current.getPoiChunk())) {
++ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> {
++ completed.setPlain(result.left());
++ isCompleted.set(true);
++ LockSupport.unpark(waiter);
++ });
++ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
++
++ if (poiLoad != null) {
++ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ }
++ }
++ } finally {
++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
++ }
++ } finally {
++ 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
++
++ boolean interrupted = false;
++ while (!isCompleted.get()) {
++ interrupted |= Thread.interrupted();
++ LockSupport.park();
++ }
++
++ if (interrupted) {
++ Thread.currentThread().interrupt();
++ }
++
++ ret = completed.getPlain();
++ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked
++
++ ret.load();
++
++ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
++
++ return ret;
++ }
++
++ void addChangedStatuses(final List changedFullStatus) {
++ if (changedFullStatus.isEmpty()) {
++ return;
++ }
++ if (!io.papermc.paper.util.TickThread.isTickThread()) {
++ this.taskScheduler.scheduleChunkTask(() -> {
++ final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
++ pendingFullLoadUpdate.add(changedFullStatus.get(i));
++ }
++
++ ChunkHolderManager.this.processPendingFullUpdate();
++ }, PrioritisedExecutor.Priority.HIGHEST);
++ } else {
++ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
++ pendingFullLoadUpdate.add(changedFullStatus.get(i));
++ }
++ }
++ }
++
++ private void removeChunkHolder(final NewChunkHolder holder) {
++ holder.markUnloaded();
++ this.autoSaveQueue.remove(holder);
++ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
++
++ }
++
++ // note: never call while inside the chunk system, this will absolutely break everything
++ public void processUnloads() {
++ 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 unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
++ int unloadCountTentative = 0;
++ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
++ final ChunkUnloadQueue.UnloadSection section
++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
++
++ if (section == null) {
++ // removed concurrently
++ continue;
++ }
++
++ // technically reading the size field is unsafe, and it may be incorrect.
++ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible
++ // for chunks to never unload or not unload fast enough.
++ unloadCountTentative += section.chunks.size();
++ }
++
++ if (unloadCountTentative <= 0) {
++ // no work to do
++ return;
++ }
++
++ // 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 ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
++ final List stage1 = new ArrayList<>();
++ final List stage2 = new ArrayList<>();
++
++ final int sectionLowerX = sectionRef.sectionX() << sectionShift;
++ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift;
++
++ // stage 1: set up for stage 2 while holding critical locks
++ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
++ try {
++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
++ try {
++ final ChunkUnloadQueue.UnloadSection section
++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
++
++ if (section == null) {
++ // removed concurrently
++ continue;
++ }
++
++ // collect the holders to run stage 1 on
++ final int sectionCount = section.chunks.size();
++
++ if ((sectionCount + processedCount) <= toUnloadCount) {
++ // we can just drain the entire section
++
++ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) {
++ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong());
++ if (holder == null) {
++ throw new IllegalStateException();
++ }
++ stage1.add(holder);
++ }
++
++ // remove section
++ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ());
++ } else {
++ // processedCount + len = toUnloadCount
++ // we cannot drain the entire section
++ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) {
++ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong());
++ if (holder == null) {
++ throw new IllegalStateException();
++ }
++ stage1.add(holder);
++ }
++ }
++
++ // 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;
++ }
++ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1();
++ if (state == null) {
++ // can unload immediately
++ this.removeChunkHolder(chunkHolder);
++ continue;
++ }
++ stage2.add(state);
++ }
++ } finally {
++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++
++ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1
++ final List stage3 = new ArrayList<>(stage2.size());
++
++ final Boolean before = this.blockTicketUpdates();
++ try {
++ for (int i = 0, len = stage2.size(); i < len; ++i) {
++ final NewChunkHolder.UnloadState state = stage2.get(i);
++ final NewChunkHolder holder = state.holder();
++
++ holder.unloadStage2(state);
++ stage3.add(holder);
++ }
++ } finally {
++ this.unblockTicketUpdates(before);
++ }
++
++ // stage 3: actually attempt to remove the chunk holders
++ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
++ try {
++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
++ try {
++ for (int i = 0, len = stage3.size(); i < len; ++i) {
++ final NewChunkHolder holder = stage3.get(i);
++
++ if (holder.unloadStage3()) {
++ this.removeChunkHolder(holder);
++ } else {
++ // add cooldown so the next unload check is not immediately next tick
++ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
++ }
++ }
++ } finally {
++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
++ }
++ } finally {
++ this.ticketLockArea.unlock(ticketLock);
++ }
++
++ processedCount += stage1.size();
++
++ if (processedCount >= toUnloadCount) {
++ break;
++ }
++ }
++ }
++
++ public enum TicketOperationType {
++ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE
++ }
++
++ public static record TicketOperation (
++ TicketOperationType op, long chunkCoord,
++ TicketType ticketType, int ticketLevel, T identifier,
++ TicketType ticketType2, int ticketLevel2, V identifier2
++ ) {
++
++ private TicketOperation(TicketOperationType op, long chunkCoord,
++ TicketType ticketType, int ticketLevel, T identifier) {
++ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null);
++ }
++
++ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) {
++ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) {
++ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) {
++ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) {
++ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) {
++ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) {
++ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier);
++ }
++
++ public static TicketOperation addIfRemovedOp(final long chunk,
++ final TicketType addType, final int addLevel, final T addIdentifier,
++ final TicketType removeType, final int removeLevel, final V removeIdentifier) {
++ return new TicketOperation<>(
++ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier,
++ removeType, removeLevel, removeIdentifier
++ );
++ }
++
++ public static TicketOperation addAndRemove(final long chunk,
++ final TicketType addType, final int addLevel, final T addIdentifier,
++ final TicketType