From 730882fca9cf468a52f348b17803fbd89acb3fb4 Mon Sep 17 00:00:00 2001
From: Spottedleaf
Date: Fri, 14 Jun 2024 17:19:25 -0700
Subject: [PATCH] Chunk System and Starlight WIP
Chunk system patch was refactored to take advantage of
newer ConcurrentUtil's concurrent long hash table (which
fixes hash collisions caused by chaining fastutil's long hash
and CHM's hash) plus some other minor improvements.
The chunk system was also merged with Starlight, which mostly
provides a small improvement to ThreadedLevelLightEngine#checkBlock
as the scheduling was rewritten.
---
leaf_notes.txt | 32 +
patches/server/0009-MC-Utils.patch | 2 +-
patches/server/0023-Timings-v2.patch | 2 +-
.../server/0066-Chunk-Save-Reattempt.patch | 6 +-
...egionFileCache-and-make-configurable.patch | 4 +-
...5-PlayerNaturallySpawnCreaturesEvent.patch | 2 +-
...ies-option-to-debug-dupe-uuid-issues.patch | 2 +-
.../0312-Tracking-Range-Improvements.patch | 2 +-
...-PlayerChunkMap-adds-crashing-server.patch | 2 +-
...nEvent-when-Player-is-actually-ready.patch | 2 +-
...primise-map-impl-for-tracked-players.patch | 2 +-
...-data-to-disk-if-it-serializes-witho.patch | 8 +-
.../0752-Fix-a-bunch-of-vanilla-bugs.patch | 2 +-
.../0781-Player-Entity-Tracking-Events.patch | 2 +-
...ntity-tracking-range-by-Y-coordinate.patch | 2 +-
...k-if-we-can-see-non-visible-entities.patch | 2 +-
...llocation-of-Vec3D-by-entity-tracker.patch | 2 +-
...Chunk-System-Starlight-from-Moonrise.patch | 28736 ++++++++++++++++
...> 0989-Rewrite-dataconverter-system.patch} | 42 +-
...90-disable-forced-empty-world-ticks.patch} | 4 +-
.../{0990-stubs.patch => 0991-stubs.patch} | 2 +-
.../0993-Starlight.patch | 0
.../0994-Rewrite-chunk-system.patch | 0
23 files changed, 28828 insertions(+), 32 deletions(-)
create mode 100644 leaf_notes.txt
create mode 100644 patches/server/0988-Chunk-System-Starlight-from-Moonrise.patch
rename patches/server/{0988-Rewrite-dataconverter-system.patch => 0989-Rewrite-dataconverter-system.patch} (99%)
rename patches/server/{0989-disable-forced-empty-world-ticks.patch => 0990-disable-forced-empty-world-ticks.patch} (84%)
rename patches/server/{0990-stubs.patch => 0991-stubs.patch} (94%)
rename {patches/unapplied/server => removed-patches-1-21}/0993-Starlight.patch (100%)
rename {patches/unapplied/server => removed-patches-1-21}/0994-Rewrite-chunk-system.patch (100%)
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