diff --git a/gradle.properties b/gradle.properties
index 384d36492..3924a9eb8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,10 +1,10 @@
group = net.pl3x.purpur
-version = 1.17-R0.1-SNAPSHOT
+version = 1.17.1-R0.1-SNAPSHOT
-mcVersion = 1.17
+mcVersion = 1.17.1
packageVersion = 1_17_R1
-paperCommit = cc063e1f09c116c9fc9786b40f7c36577a6a7264
+paperCommit = 091319d165bbc311443790aa03ff95a4ccc01780
org.gradle.parallel = true
org.gradle.vfs.watch = false
diff --git a/patches/api/0001-Tuinity-API-Changes.patch b/patches/api/0001-Tuinity-API-Changes.patch
index 26787ba04..99624a52c 100644
--- a/patches/api/0001-Tuinity-API-Changes.patch
+++ b/patches/api/0001-Tuinity-API-Changes.patch
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
-index 07c9635ea2e09e823ed7fa164ed854af41fab7dc..8482c4a664a02614fd59f74a4f9dd6d2e32003f5 100644
+index f05edac8cdd33daaf1d15a526be4d2ac2b08846d..8776b8368d2046dee02e927de8249030bdddf2ee 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -1626,6 +1626,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
@@ -35,106 +35,3 @@ index 07c9635ea2e09e823ed7fa164ed854af41fab7dc..8482c4a664a02614fd59f74a4f9dd6d2
/**
* Sends the component to the player
*
-diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java
-index 8ae9198ba7fdb006dc420504a984627add20dbb5..4017cc64532a9a8e42c3a6492878cd96db13fcb3 100644
---- a/src/main/java/org/bukkit/World.java
-+++ b/src/main/java/org/bukkit/World.java
-@@ -3639,6 +3639,26 @@ public interface World extends PluginMessageRecipient, Metadatable, net.kyori.ad
- * @param viewDistance view distance in [2, 32]
- */
- void setNoTickViewDistance(int viewDistance);
-+
-+ // Tuinity start - add view distances
-+ /**
-+ * Gets the sending view distance for this world.
-+ *
-+ * Sending view distance is the view distance where chunks will load in for players in this world.
-+ *
-+ * @return The sending view distance for this world.
-+ */
-+ public int getSendViewDistance();
-+
-+ /**
-+ * Sets the sending view distance for this world.
-+ *
-+ * Sending view distance is the view distance where chunks will load in for players in this world.
-+ *
-+ * @param viewDistance view distance in [2, 32] or -1
-+ */
-+ public void setSendViewDistance(int viewDistance);
-+ // Tuinity end - add view distances
- // Paper end - view distance api
-
- // Spigot start
-diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index da83b4cbed0be6f693c7cbb1cc032356f12d7883..51c334f68052f58fbb9c10fb9ed31ab42780ceac 100644
---- a/src/main/java/org/bukkit/entity/Player.java
-+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -1818,23 +1818,63 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
- * Gets the view distance for this player
- *
- * @return the player's view distance
-- * @deprecated This is unimplemented and will throw an exception at runtime. The {@link org.bukkit.World World}-based methods still work.
-+ * // Tuinity - implemented
- * @see org.bukkit.World#getViewDistance()
- * @see org.bukkit.World#getNoTickViewDistance()
- */
-- @Deprecated
-+ //@Deprecated // Tuinity - implemented
- public int getViewDistance();
-
- /**
- * Sets the view distance for this player
- *
- * @param viewDistance the player's view distance
-- * @deprecated This is unimplemented and will throw an exception at runtime. The {@link org.bukkit.World World}-based methods still work.
-+ * // Tuinity - implemented
- * @see org.bukkit.World#setViewDistance(int)
- * @see org.bukkit.World#setNoTickViewDistance(int)
- */
-- @Deprecated
-+ //@Deprecated // Tuinity - implemented
- public void setViewDistance(int viewDistance);
-+
-+ // Tuinity start - add view distances api
-+ /**
-+ * Gets the no-ticking view distance for this player.
-+ *
-+ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not
-+ * be set to tick.
-+ *
-+ * @return The no-tick view distance for this player.
-+ */
-+ public int getNoTickViewDistance();
-+
-+ /**
-+ * Sets the no-ticking view distance for this player.
-+ *
-+ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not
-+ * be set to tick.
-+ *
-+ * @param viewDistance view distance in [2, 32] or -1
-+ */
-+ public void setNoTickViewDistance(int viewDistance);
-+
-+ /**
-+ * Gets the sending view distance for this player.
-+ *
-+ * Sending view distance is the view distance where chunks will load in for players.
-+ *
-+ * @return The sending view distance for this player.
-+ */
-+ public int getSendViewDistance();
-+
-+ /**
-+ * Sets the sending view distance for this player.
-+ *
-+ * Sending view distance is the view distance where chunks will load in for players.
-+ *
-+ * @param viewDistance view distance in [2, 32] or -1
-+ */
-+ public void setSendViewDistance(int viewDistance);
-+ // Tuinity end - add view distances api
- // Paper end
-
- /**
diff --git a/patches/api/0002-Build-System-Changes.patch b/patches/api/0002-Build-System-Changes.patch
index 9b46f9429..c5f0d5db5 100644
--- a/patches/api/0002-Build-System-Changes.patch
+++ b/patches/api/0002-Build-System-Changes.patch
@@ -6,10 +6,10 @@ Subject: [PATCH] Build System Changes
todo: merge with rebrand patch
diff --git a/build.gradle.kts b/build.gradle.kts
-index e142072f31a41b25ac637970f79e71ab70c2f28c..407bf7c09304c76ef1dbec43bb5d665f02ce4840 100644
+index 7ad3e5153718f6d4ce8293a9790bc3c1158aeb8e..309d201b7d551efd4a5903e6d990b0e718af6a78 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
-@@ -27,6 +27,7 @@ dependencies {
+@@ -29,6 +29,7 @@ dependencies {
api("org.ow2.asm:asm:9.0")
api("org.ow2.asm:asm-commons:9.0")
api("org.apache.logging.log4j:log4j-api:2.14.1") // Paper
diff --git a/patches/api/0009-AFK-API.patch b/patches/api/0009-AFK-API.patch
index 6f2ad3095..bc5bf72d5 100644
--- a/patches/api/0009-AFK-API.patch
+++ b/patches/api/0009-AFK-API.patch
@@ -81,10 +81,10 @@ index 0000000000000000000000000000000000000000..0c8b3e5e4ba412624357ea5662a78862
+ }
+}
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 51c334f68052f58fbb9c10fb9ed31ab42780ceac..13970eb20ed6bbc12720cf6c0b609e6cf2952933 100644
+index 37ad0c478b83ecf63edfe62b5b2dcd81d6fe1e77..40782217f40146e2509e7808d7b354269a56dc1c 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2166,4 +2166,25 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2126,4 +2126,25 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
@Override
Spigot spigot();
// Spigot end
diff --git a/patches/api/0018-Player-invulnerabilities.patch b/patches/api/0018-Player-invulnerabilities.patch
index 6195d067d..ab0033bfb 100644
--- a/patches/api/0018-Player-invulnerabilities.patch
+++ b/patches/api/0018-Player-invulnerabilities.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Player invulnerabilities
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 13970eb20ed6bbc12720cf6c0b609e6cf2952933..4f9a182f777712b6d266b7c1acee541f85740085 100644
+index 40782217f40146e2509e7808d7b354269a56dc1c..d212021efd579bf5a527b6ef923279b055eb7754 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2186,5 +2186,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2146,5 +2146,26 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* Reset the idle timer back to 0
*/
void resetIdleTimer();
diff --git a/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch b/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
index 07df90df4..67bd8d6f8 100644
--- a/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
+++ b/patches/api/0032-Fix-javadoc-warnings-missing-param-and-return.patch
@@ -489,7 +489,7 @@ index 3afd5f5c0208a4ee93b5dbfc2aab2b9d2e8a7544..7838731e0e16bdccfb79e74ceb64148f
/**
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
-index bab0c0d660a01e912d0a88aefbacf20895f4ef61..5fcf5e0e746d579b6c3abf7148554463770332bd 100644
+index 59734f9d2a2aa59f683b8fae5b6c19e61c2bc40d..5ca1d420a14797d8b2a867f867d19188084e7fa5 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -1599,6 +1599,9 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
@@ -566,7 +566,7 @@ index afb7b136b461202026290624836446cff9f9e45d..087579fdff09237409c9f80446e7a15a
/**
diff --git a/src/main/java/org/bukkit/WorldCreator.java b/src/main/java/org/bukkit/WorldCreator.java
-index e6a83252f42da31ad38f8dc1beccc7aa2c3f54b8..f3b107210473f1707b051c15771ce3bf2a62f171 100644
+index c0454d977119b28115b7698a2c4287f0f56efa55..445c1c13a85f7abb5fd319f9aeb05572bb1f63f6 100644
--- a/src/main/java/org/bukkit/WorldCreator.java
+++ b/src/main/java/org/bukkit/WorldCreator.java
@@ -71,6 +71,8 @@ public class WorldCreator {
@@ -587,20 +587,6 @@ index e6a83252f42da31ad38f8dc1beccc7aa2c3f54b8..f3b107210473f1707b051c15771ce3bf
*/
@NotNull
public static WorldCreator ofKey(@NotNull NamespacedKey worldKey) {
-@@ -293,11 +297,8 @@ public class WorldCreator {
- * is as follows:
- * {"structures": {"structures": {"village": {"salt": 8015723, "spacing": 32, "separation": 8}}}, "layers": [{"block": "stone", "height": 1}, {"block": "grass", "height": 1}], "biome":"plains"}
- *
-- * @see Custom
-- * dimension (scroll to "When the generator ID type is
-- * minecraft:flat)"
-- * @param generatorSettings The settings that should be used by the
-- * generator
-+ * @see Custom dimension
-+ * @param generatorSettings The settings that should be used by the generator
- * @return This object, for chaining
- */
- @NotNull
diff --git a/src/main/java/org/bukkit/advancement/AdvancementDisplay.java b/src/main/java/org/bukkit/advancement/AdvancementDisplay.java
index c2e161e8e14d9949165055b6051708c048e68338..2bb3b525a3974b6ccd223b2ba272933c1617ceac 100644
--- a/src/main/java/org/bukkit/advancement/AdvancementDisplay.java
@@ -766,7 +752,7 @@ index 2e17b2d4f759531fbe9ee8e9b00c839186af09ca..9382234722792b5920a2456187e07958
/**
diff --git a/src/main/java/org/bukkit/entity/ArmorStand.java b/src/main/java/org/bukkit/entity/ArmorStand.java
-index 2f0c6af7fa6688a98d6aa0bd3f0e6556af8330d0..b38c69482e3112e0cd626bcb17f4523c541b748f 100644
+index 21c8ec6d8b795799b5b110be57ffd27fc8dcabe3..d3eb843fb6a7257152120371c4f317133f63de4d 100644
--- a/src/main/java/org/bukkit/entity/ArmorStand.java
+++ b/src/main/java/org/bukkit/entity/ArmorStand.java
@@ -7,6 +7,9 @@ import org.bukkit.util.EulerAngle;
@@ -963,10 +949,10 @@ index a6a7429ed2e1eefb2b12b7480ed74fcc3963a864..e8027e1d505dda6effbb1698550016e8
NORMAL(false),
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 4f9a182f777712b6d266b7c1acee541f85740085..93d9e1b7b4ac9b9ccee7006375d5a96eaf9508ca 100644
+index d212021efd579bf5a527b6ef923279b055eb7754..bc03bbf9fa61b98bc6c208ab4a0e653f4b0ea472 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -1988,6 +1988,8 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -1948,6 +1948,8 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
void resetCooldown();
/**
@@ -975,7 +961,7 @@ index 4f9a182f777712b6d266b7c1acee541f85740085..93d9e1b7b4ac9b9ccee7006375d5a96e
* @return the client option value of the player
*/
@NotNull
-@@ -2027,6 +2029,9 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -1987,6 +1989,9 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
// Paper end
// Spigot start
@@ -1137,7 +1123,7 @@ index 25d26e3fe713311e66d7e634a6c32af61f4cef59..2825263c102d3f9ed37f6884e09ec5ef
/**
diff --git a/src/main/java/org/bukkit/event/block/CauldronLevelChangeEvent.java b/src/main/java/org/bukkit/event/block/CauldronLevelChangeEvent.java
-index 2519e3eb9c6274476310913fdcb765c490d50962..44b07ced26449983a58e936c5d6d5ed2f7022fb4 100644
+index 668cc4b7d8a15ae345d130f8164107f000b6fe22..ebfeb3c8ebaf53a7fb6349f0f1480efc34c971e9 100644
--- a/src/main/java/org/bukkit/event/block/CauldronLevelChangeEvent.java
+++ b/src/main/java/org/bukkit/event/block/CauldronLevelChangeEvent.java
@@ -12,6 +12,9 @@ import org.bukkit.event.HandlerList;
@@ -1315,7 +1301,7 @@ index 418f9391d86fff0d0a75da0574edccbb29aa9931..921d964d7e40e7710b5a5db18bd9329c
/**
* Triggered by the /summon command.
diff --git a/src/main/java/org/bukkit/help/HelpTopicComparator.java b/src/main/java/org/bukkit/help/HelpTopicComparator.java
-index 75bb69283f509e8f4fec772714a509a51be9de19..e156847f5b7b86155a7a0a0b8cefd8ac1530171e 100644
+index e1f4930f4d7cf657a75282b4c3480cabaaee2765..00b3b1d65f80d8e17ca1a40b704ece57a4776f8e 100644
--- a/src/main/java/org/bukkit/help/HelpTopicComparator.java
+++ b/src/main/java/org/bukkit/help/HelpTopicComparator.java
@@ -31,6 +31,9 @@ public final class HelpTopicComparator implements Comparator {
@@ -1326,7 +1312,7 @@ index 75bb69283f509e8f4fec772714a509a51be9de19..e156847f5b7b86155a7a0a0b8cefd8ac
+ * Topic name comparator
+ */
public static final class TopicNameComparator implements Comparator {
- private TopicNameComparator(){}
+ private TopicNameComparator() {}
diff --git a/src/main/java/org/bukkit/inventory/ArmoredHorseInventory.java b/src/main/java/org/bukkit/inventory/ArmoredHorseInventory.java
index 163ffe8ff76ded6265d865901d5110fb6a56950d..36145294db34d273bb767cc928453b765a30e9db 100644
diff --git a/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch b/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch
index be84d0be3..068c8d39b 100644
--- a/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch
+++ b/patches/api/0038-Conflict-on-change-for-adventure-deprecations.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Conflict on change for adventure deprecations
diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
-index bf86f402bc099ee190c809220f211ed59b3e9613..82978d66cc7d6214cf84f566918bfaf6bcbe1118 100644
+index cc739092191c0d1736b1b25622104d23ab171ce8..c6b3bf5b16c26f652c0649c7a2c41f700ac2c01e 100644
--- a/src/main/java/org/bukkit/Bukkit.java
+++ b/src/main/java/org/bukkit/Bukkit.java
@@ -346,7 +346,7 @@ public final class Bukkit {
@@ -72,7 +72,7 @@ index bf86f402bc099ee190c809220f211ed59b3e9613..82978d66cc7d6214cf84f566918bfaf6
return server.getShutdownMessage();
}
diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
-index 5fcf5e0e746d579b6c3abf7148554463770332bd..2f9ee4cf3b2163dbe086b4d40179bfa64ebfdecf 100644
+index 5ca1d420a14797d8b2a867f867d19188084e7fa5..f82023103a328f234c990d6787c9b97fd1e82590 100644
--- a/src/main/java/org/bukkit/Server.java
+++ b/src/main/java/org/bukkit/Server.java
@@ -270,7 +270,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
@@ -139,7 +139,7 @@ index 5fcf5e0e746d579b6c3abf7148554463770332bd..2f9ee4cf3b2163dbe086b4d40179bfa6
/**
diff --git a/src/main/java/org/bukkit/block/Sign.java b/src/main/java/org/bukkit/block/Sign.java
-index cdcf02ff9e80f5908a8fa22e82701445d5e2d298..83eba2421cdfa56c2f5b9ebaac18d56360164fed 100644
+index c8d37184d8e882a4084a1bfef85faa330588600b..46bae5c13ce2b973b114682f6a338981eb8d95bf 100644
--- a/src/main/java/org/bukkit/block/Sign.java
+++ b/src/main/java/org/bukkit/block/Sign.java
@@ -48,7 +48,7 @@ public interface Sign extends TileState, Colorable {
@@ -170,7 +170,7 @@ index cdcf02ff9e80f5908a8fa22e82701445d5e2d298..83eba2421cdfa56c2f5b9ebaac18d563
/**
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 93d9e1b7b4ac9b9ccee7006375d5a96eaf9508ca..2c9860d7e55d254e3b201f4580ac400090ffe6e1 100644
+index bc03bbf9fa61b98bc6c208ab4a0e653f4b0ea472..5bb39f78a7e87dddc38b1a641438ebcc2de945b7 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
@@ -75,7 +75,7 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
@@ -370,7 +370,7 @@ index 09f0fa8f2aac16b2c2d848089e228af2d09f9090..e10ff4ab0c649289af9eafe83ef9268e
return title;
}
diff --git a/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java b/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
-index 77aefda5aac4602bf5bf71c29600e7450defdd4e..240552d61ae12fbec826f771f0f366500e72d941 100644
+index 694a81769076ea58aae9f14f076ab80c9952c957..2d5066a0e24b671a6b287c34603b371ed51d7be7 100644
--- a/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
+++ b/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
@@ -179,7 +179,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
@@ -472,10 +472,10 @@ index 05ecfd8c133e72d198faeeded8c757c231c871cc..e57020767879b51f212d7a3e563a386a
this.leaveMessage = org.bukkit.Bukkit.getUnsafe().legacyComponentSerializer().deserialize(leaveMessage); // Paper
}
diff --git a/src/main/java/org/bukkit/event/player/PlayerLocaleChangeEvent.java b/src/main/java/org/bukkit/event/player/PlayerLocaleChangeEvent.java
-index 84521186404b8e43c81a2f9513dce2be40d27840..8c65e9e1476d27fc55419290fb53e46dee9b304d 100644
+index ebd499c1a2d11ea068e8c374edbc3967e4cece3d..61895d8d9f01f7ad0409a1cbd902e8a21472d6d4 100644
--- a/src/main/java/org/bukkit/event/player/PlayerLocaleChangeEvent.java
+++ b/src/main/java/org/bukkit/event/player/PlayerLocaleChangeEvent.java
-@@ -37,7 +37,7 @@ public class PlayerLocaleChangeEvent extends PlayerEvent {
+@@ -36,7 +36,7 @@ public class PlayerLocaleChangeEvent extends PlayerEvent {
* @deprecated in favour of {@link #locale()}
*/
@NotNull
@@ -746,7 +746,7 @@ index ed0bc2024a0bb85837e25f75ae89d1fe257b2e60..f6e831f844e1fe99a2617bd64c2290d1
this.caption = caption == null ? null : org.bukkit.Bukkit.getUnsafe().legacyComponentSerializer().deserialize(caption); // Paper
}
diff --git a/src/main/java/org/bukkit/scoreboard/Objective.java b/src/main/java/org/bukkit/scoreboard/Objective.java
-index 58bddb11fd534e7c33a4ffd7b72b055ba92c767a..a1b6b1123808378d58c855cacac391ce97df6f19 100644
+index 6279957b9bc6d22881f092eabf3a99831d85e3ee..24019ed4ff289b9d33da39cee66ea91831bc33c0 100644
--- a/src/main/java/org/bukkit/scoreboard/Objective.java
+++ b/src/main/java/org/bukkit/scoreboard/Objective.java
@@ -47,7 +47,7 @@ public interface Objective {
@@ -768,7 +768,7 @@ index 58bddb11fd534e7c33a4ffd7b72b055ba92c767a..a1b6b1123808378d58c855cacac391ce
/**
diff --git a/src/main/java/org/bukkit/scoreboard/Scoreboard.java b/src/main/java/org/bukkit/scoreboard/Scoreboard.java
-index f09ff32cc3ffc16af379a378b1948991435393e8..e9db79d10522895e6f119c0cc87eec1cbc45ba6e 100644
+index 93089ce61d2e1888df13b7c9629a79cd6f5f767a..ea0480064068f34ea34d4b68ef12d0f860e58d80 100644
--- a/src/main/java/org/bukkit/scoreboard/Scoreboard.java
+++ b/src/main/java/org/bukkit/scoreboard/Scoreboard.java
@@ -89,7 +89,7 @@ public interface Scoreboard {
@@ -790,7 +790,7 @@ index f09ff32cc3ffc16af379a378b1948991435393e8..e9db79d10522895e6f119c0cc87eec1c
/**
diff --git a/src/main/java/org/bukkit/scoreboard/Team.java b/src/main/java/org/bukkit/scoreboard/Team.java
-index f0af10a5b9ad048be197ed5ec6c8ed2672eb3dd5..705b2268b1c227b34852c14601381230dc626a08 100644
+index 30fce0df75494eb9b7409f08ea3d6ff894f7c79f..12789cd0ee7d6e29fa122f040e8ce0cac57945a7 100644
--- a/src/main/java/org/bukkit/scoreboard/Team.java
+++ b/src/main/java/org/bukkit/scoreboard/Team.java
@@ -110,7 +110,7 @@ public interface Team {
diff --git a/patches/api/0042-Flying-Fall-Damage-API.patch b/patches/api/0042-Flying-Fall-Damage-API.patch
index 6fabd5900..e7d50ea97 100644
--- a/patches/api/0042-Flying-Fall-Damage-API.patch
+++ b/patches/api/0042-Flying-Fall-Damage-API.patch
@@ -5,10 +5,10 @@ Subject: [PATCH] Flying Fall Damage API
diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
-index 2c9860d7e55d254e3b201f4580ac400090ffe6e1..fd57b3ebadce95c9534fa532b7a5c6c57660bc3e 100644
+index 5bb39f78a7e87dddc38b1a641438ebcc2de945b7..87d036d914fc2ca9b3056d7c5103d2e1ceb6e51e 100644
--- a/src/main/java/org/bukkit/entity/Player.java
+++ b/src/main/java/org/bukkit/entity/Player.java
-@@ -2212,5 +2212,19 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+@@ -2172,5 +2172,19 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @param invulnerableTicks Invulnerable ticks remaining
*/
void setSpawnInvulnerableTicks(int invulnerableTicks);
diff --git a/patches/server/0001-Tuinity-Server-Changes.patch b/patches/server/0001-Tuinity-Server-Changes.patch
index fcbf9cf73..f19470a64 100644
--- a/patches/server/0001-Tuinity-Server-Changes.patch
+++ b/patches/server/0001-Tuinity-Server-Changes.patch
@@ -89,19039 +89,6 @@ index 5540da58e66f83b283863d3158a9b4ab5ba636db..ab8de6c4e3c0bea2b9f498da00adf88e
standardInput = System.`in`
workingDir = rootProject.layout.projectDirectory.dir(
providers.gradleProperty("runWorkDir").forUseAtConfigurationTime().orElse("run")
-diff --git a/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..9efbdba758aebcad3454a9a52c8a7eae4b7fc7eb
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java
-@@ -0,0 +1,283 @@
-+package ca.spottedleaf.starlight.light;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.*;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+import java.util.ArrayList;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.stream.Collectors;
-+
-+public final class BlockStarLightEngine extends StarLightEngine {
-+
-+ public BlockStarLightEngine(final Level world) {
-+ super(false, world);
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setBlockEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getBlockNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setBlockNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
-+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
-+ // in increases, and thus the existing sky block check will actually correctly propagate light through
-+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
-+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
-+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
-+ // of vanilla data management we "hide" them.
-+ nibble.setHidden();
-+ }
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
-+ }
-+ } else {
-+ nibble.setNonNull();
-+ }
-+ }
-+
-+ @Override
-+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change emitted light
-+ // blocks can change direction of propagation
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
-+
-+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
-+ // this accounts for change in emitted light that would cause an increase
-+ if (emittedLevel != 0) {
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+ }
-+ // this also accounts for a change in emitted light that would cause a decrease
-+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
-+ // as it checks all neighbours (even if current level is 0)
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ // always keep sided transparent false here, new block might be conditionally transparent which would
-+ // prevent us from decreasing sources in the directions where the new block is opaque
-+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
-+ // catch that and fix it.
-+ );
-+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int level = centerState.getLightEmission() & 0xF;
-+
-+ if (level >= (15 - 1) || level > expect) {
-+ return level;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState conditionallyOpaqueState;
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ if (opacity == -1) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else if (opacity >= 15) {
-+ return level;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ opacity = Math.max(1, opacity);
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ // passed transparency,
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) {
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected Iterator getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
-+ if (chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) {
-+ // implementation on Chunk is pretty awful, so write our own here. The big optimisation is
-+ // skipping empty sections, and the far more optimised reading of types.
-+ List sources = new ArrayList<>();
-+
-+ int offX = chunk.getPos().x << 4;
-+ int offZ = chunk.getPos().z << 4;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
-+ final LevelChunkSection section = sections[sectionY - this.minSection];
-+ if (section == null || section.isEmpty()) {
-+ // no sources in empty sections
-+ continue;
-+ }
-+ final PalettedContainer states = section.states;
-+ final int offY = sectionY << 4;
-+
-+ for (int index = 0; index < (16 * 16 * 16); ++index) {
-+ final BlockState state = states.get(index);
-+ if (state.getLightEmission() <= 0) {
-+ continue;
-+ }
-+
-+ // index = x | (z << 4) | (y << 8)
-+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
-+ }
-+ }
-+
-+ return sources.iterator();
-+ } else {
-+ // world gen and lighting run in parallel, and if lighting keeps up it can be lighting chunks that are
-+ // being generated. In the nether, lava will add a lot of sources. This resulted in quite a few CME crashes.
-+ // So all we do spinloop until we can collect a list of sources, and even if it is out of date we will pick up
-+ // the missing sources from checkBlock.
-+ for (;;) {
-+ try {
-+ return chunk.getLights().collect(Collectors.toList()).iterator();
-+ } catch (final Exception cme) {
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ // setup sources
-+ final int emittedMask = this.emittedLightMask;
-+ for (final Iterator positions = this.getSources(lightAccess, chunk); positions.hasNext();) {
-+ final BlockPos pos = positions.next();
-+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+
-+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
-+ // some other source is brighter
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLight & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+
-+
-+ // propagation wont set this for us
-+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
-+ }
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ // verify neighbour edges
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ } else {
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..174dc7ffa66258da0b867fba5c54880e81daa6ce
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java
-@@ -0,0 +1,439 @@
-+package ca.spottedleaf.starlight.light;
-+
-+import net.minecraft.world.level.chunk.DataLayer;
-+
-+import java.util.ArrayDeque;
-+import java.util.Arrays;
-+
-+// SWMR -> Single Writer Multi Reader Nibble Array
-+public final class SWMRNibbleArray {
-+
-+ /*
-+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
-+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
-+ * nibbles can be written to.
-+ *
-+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
-+ *
-+ * Initialised nibble - Has light data.
-+ */
-+
-+ protected static final int INIT_STATE_NULL = 0; // null
-+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
-+ protected static final int INIT_STATE_INIT = 2; // initialised
-+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
-+
-+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
-+ // this allows us to maintain only 1 byte array when we're not updating
-+ static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
-+
-+ private static byte[] allocateBytes() {
-+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
-+ if (inPool != null) {
-+ return inPool;
-+ }
-+
-+ return new byte[ARRAY_SIZE];
-+ }
-+
-+ private static void freeBytes(final byte[] bytes) {
-+ WORKING_BYTES_POOL.get().addFirst(bytes);
-+ }
-+
-+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
-+ if (nibble == null) {
-+ return new SWMRNibbleArray(null, true);
-+ } else if (nibble.isEmpty()) {
-+ return new SWMRNibbleArray();
-+ } else {
-+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
-+ }
-+ }
-+
-+ protected int stateUpdating;
-+ protected volatile int stateVisible;
-+
-+ protected byte[] storageUpdating;
-+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
-+ protected byte[] storageVisible;
-+
-+ public SWMRNibbleArray() {
-+ this(null, false); // lazy init
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes) {
-+ this(bytes, false);
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final int state) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
-+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
-+ }
-+ this.stateUpdating = this.stateVisible = state;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ StringBuilder stringBuilder = new StringBuilder();
-+ stringBuilder.append("State: ");
-+ switch (this.stateVisible) {
-+ case INIT_STATE_NULL:
-+ stringBuilder.append("null");
-+ break;
-+ case INIT_STATE_UNINIT:
-+ stringBuilder.append("uninitialised");
-+ break;
-+ case INIT_STATE_INIT:
-+ stringBuilder.append("initialised");
-+ break;
-+ case INIT_STATE_HIDDEN:
-+ stringBuilder.append("hidden");
-+ break;
-+ default:
-+ stringBuilder.append("unknown");
-+ break;
-+ }
-+ stringBuilder.append("\nData:\n");
-+
-+ final byte[] data = this.storageVisible;
-+ if (data != null) {
-+ for (int i = 0; i < 4096; ++i) {
-+ // Copied from NibbleArray#toString
-+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
-+
-+ stringBuilder.append(Integer.toHexString(level));
-+ if ((i & 15) == 15) {
-+ stringBuilder.append("\n");
-+ }
-+
-+ if ((i & 255) == 255) {
-+ stringBuilder.append("\n");
-+ }
-+ }
-+ } else {
-+ stringBuilder.append("null");
-+ }
-+
-+ return stringBuilder.toString();
-+ }
-+
-+ public SaveState getSaveState() {
-+ synchronized (this) {
-+ final int state = this.stateVisible;
-+ final byte[] data = this.storageVisible;
-+ if (state == INIT_STATE_NULL) {
-+ return null;
-+ }
-+ if (state == INIT_STATE_UNINIT) {
-+ return new SaveState(null, state);
-+ }
-+ final boolean zero = isAllZero(data);
-+ if (zero) {
-+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
-+ } else {
-+ return new SaveState(data.clone(), state);
-+ }
-+ }
-+ }
-+
-+ protected static boolean isAllZero(final byte[] data) {
-+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
-+ byte whole = data[i << 4];
-+
-+ for (int k = 1; k < (1 << 4); ++k) {
-+ whole |= data[(i << 4) | k];
-+ }
-+
-+ if (whole != 0) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: updating on src, updating on other
-+ public void extrudeLower(final SWMRNibbleArray other) {
-+ if (other.stateUpdating == INIT_STATE_NULL) {
-+ throw new IllegalArgumentException();
-+ }
-+
-+ if (other.storageUpdating == null) {
-+ this.setUninitialised();
-+ return;
-+ }
-+
-+ final byte[] src = other.storageUpdating;
-+ final byte[] into;
-+
-+ if (this.storageUpdating != null) {
-+ into = this.storageUpdating;
-+ } else {
-+ this.storageUpdating = into = allocateBytes();
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+
-+ final int start = 0;
-+ final int end = (15 | (15 << 4)) >>> 1;
-+
-+ /* x | (z << 4) | (y << 8) */
-+ for (int y = 0; y <= 15; ++y) {
-+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
-+ }
-+ }
-+
-+ // operation type: updating
-+ public void setFull() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setZero() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setNonNull() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_NULL) {
-+ return;
-+ }
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public void setNull() {
-+ this.stateUpdating = INIT_STATE_NULL;
-+ if (this.updatingDirty && this.storageUpdating != null) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setUninitialised() {
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ if (this.storageUpdating != null && this.updatingDirty) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setHidden() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_INIT) {
-+ this.setNull();
-+ } else {
-+ this.stateUpdating = INIT_STATE_HIDDEN;
-+ }
-+ }
-+
-+ // operation type: updating
-+ public boolean isDirty() {
-+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
-+ }
-+
-+ // operation type: updating
-+ public boolean isNullNibbleUpdating() {
-+ return this.stateUpdating == INIT_STATE_NULL;
-+ }
-+
-+ // operation type: visible
-+ public boolean isNullNibbleVisible() {
-+ return this.stateVisible == INIT_STATE_NULL;
-+ }
-+
-+ // opeartion type: updating
-+ public boolean isUninitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isUninitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isInitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isInitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenUpdating() {
-+ return this.stateUpdating == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenVisible() {
-+ return this.stateVisible == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ protected void swapUpdatingAndMarkDirty() {
-+ if (this.updatingDirty) {
-+ return;
-+ }
-+
-+ if (this.storageUpdating == null) {
-+ this.storageUpdating = allocateBytes();
-+ Arrays.fill(this.storageUpdating, (byte)0);
-+ } else {
-+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
-+ }
-+
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public boolean updateVisible() {
-+ if (!this.isDirty()) {
-+ return false;
-+ }
-+
-+ synchronized (this) {
-+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
-+ this.storageVisible = null;
-+ } else {
-+ if (this.storageVisible == null) {
-+ this.storageVisible = this.storageUpdating.clone();
-+ } else {
-+ if (this.storageUpdating != this.storageVisible) {
-+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
-+ }
-+ }
-+
-+ if (this.storageUpdating != this.storageVisible) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = this.storageVisible;
-+ }
-+ this.updatingDirty = false;
-+ this.stateVisible = this.stateUpdating;
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: visible
-+ public DataLayer toVanillaNibble() {
-+ synchronized (this) {
-+ switch (this.stateVisible) {
-+ case INIT_STATE_HIDDEN:
-+ case INIT_STATE_NULL:
-+ return null;
-+ case INIT_STATE_UNINIT:
-+ return new DataLayer();
-+ case INIT_STATE_INIT:
-+ return new DataLayer(this.storageVisible.clone());
-+ default:
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ /* x | (z << 4) | (y << 8) */
-+
-+ // operation type: updating
-+ public int getUpdating(final int x, final int y, final int z) {
-+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: updating
-+ public int getUpdating(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] bytes = this.storageUpdating;
-+ if (bytes == null) {
-+ return 0;
-+ }
-+ final byte value = bytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int x, final int y, final int z) {
-+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int index) {
-+ synchronized (this) {
-+ // indices range from 0 -> 4096
-+ final byte[] visibleBytes = this.storageVisible;
-+ if (visibleBytes == null) {
-+ return 0;
-+ }
-+ final byte value = visibleBytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+ }
-+
-+ // operation type: updating
-+ public void set(final int x, final int y, final int z, final int value) {
-+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int index, final int value) {
-+ if (!this.updatingDirty) {
-+ this.swapUpdatingAndMarkDirty();
-+ }
-+ final int shift = (index & 1) << 2;
-+ final int i = index >>> 1;
-+
-+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
-+ }
-+
-+ public static final class SaveState {
-+
-+ public final byte[] data;
-+ public final int state;
-+
-+ public SaveState(final byte[] data, final int state) {
-+ this.data = data;
-+ this.state = state;
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..143810dc53782a9a6d870089d7bd5c3006a565b3
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java
-@@ -0,0 +1,715 @@
-+package ca.spottedleaf.starlight.light;
-+
-+import com.tuinity.tuinity.util.WorldUtil;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.Set;
-+
-+public final class SkyStarLightEngine extends StarLightEngine {
-+
-+ /*
-+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
-+
-+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
-+
-+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
-+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
-+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
-+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
-+ How do we solve this?
-+
-+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
-+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
-+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
-+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
-+ to see if any of its nibbles need to be de-initialised.
-+
-+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
-+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
-+
-+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
-+ around those.
-+ */
-+
-+ protected final int[] heightMapBlockChange = new int[16 * 16];
-+ {
-+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
-+ }
-+
-+ protected final boolean[] nullPropagationCheckCache;
-+
-+ public SkyStarLightEngine(final Level world) {
-+ super(true, world);
-+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
-+ }
-+ }
-+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ nibble.setNull();
-+ }
-+ }
-+
-+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
-+ if (!currNibble.isNullNibbleUpdating()) {
-+ // already initialised
-+ return;
-+ }
-+
-+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = this.minLightSection - 1;
-+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
-+ if (emptinessMap == null) {
-+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
-+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
-+ if (current == null || current == EMPTY_CHUNK_SECTION) {
-+ continue;
-+ }
-+ } else {
-+ if (emptinessMap[currY - this.minSection]) {
-+ continue;
-+ }
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (chunkY > lowestY) {
-+ // we need to set this one to full
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ nibble.setNonNull();
-+ nibble.setFull();
-+ return;
-+ }
-+
-+ if (extrude) {
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ currNibble.setNonNull();
-+ currNibble.extrudeLower(nibble);
-+ break;
-+ }
-+ }
-+ } else {
-+ currNibble.setNonNull();
-+ }
-+ }
-+
-+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (nibble != null && nibble.isNullNibbleUpdating()) {
-+ // stop propagation in these areas
-+ this.nibbleCache[index] = null;
-+ nibble.updateVisible();
-+ }
-+ }
-+ }
-+
-+ // rets whether neighbours were init'd
-+
-+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final boolean extrudeInitialised) {
-+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
-+ // non-null. Propagation to these neighbours is necessary.
-+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
-+ // this nibble would be initialised). So, we don't have to initialise
-+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
-+ // to the neighbours on this horizontal plane are blocked.
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
-+ return false;
-+ }
-+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
-+
-+ // check horizontal neighbours
-+ boolean needInitNeighbours = false;
-+ neighbour_search:
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ needInitNeighbours = true;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+
-+ if (needInitNeighbours) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
-+ }
-+ }
-+ }
-+
-+ return needInitNeighbours;
-+ }
-+
-+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
-+ final int chunkX = worldX >> 4;
-+ int chunkY = worldY >> 4;
-+ final int chunkZ = worldZ >> 4;
-+
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, worldY, worldZ);
-+ }
-+
-+ for (;;) {
-+ if (++chunkY > this.maxLightSection) {
-+ return 15;
-+ }
-+
-+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, 0, worldZ);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setSkyEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getSkyNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setSkyNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ // can only use chunks for sky stuff if their sections have been init'd
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
-+ final int toSection) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (int y = toSection; y >= fromSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ final int y = (int)iterator.nextShort();
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, sections);
-+ }
-+
-+ @Override
-+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change direction of propagation
-+
-+ // same logic applies from BlockStarLightEngine#checkBlock
-+
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+
-+ if (currentLevel == 15) {
-+ // must re-propagate clobbered source
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
-+ );
-+ } else {
-+ this.setLightLevel(worldX, worldY, worldZ, 0);
-+ }
-+
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ );
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ if (expect == 15) {
-+ return expect;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ BlockState conditionallyOpaqueState;
-+ if (opacity < 0) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else {
-+ conditionallyOpaqueState = null;
-+ opacity = Math.max(1, opacity);
-+ }
-+
-+ int level = 0;
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) {
-+ this.rewriteNibbleCacheForSkylight(atChunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final int chunkX = atChunk.getPos().x;
-+ final int chunkZ = atChunk.getPos().z;
-+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
-+
-+ // setup heightmap for changes
-+ for (final BlockPos pos : positions) {
-+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
-+ final int curr = this.heightMapBlockChange[index];
-+ if (pos.getY() > curr) {
-+ this.heightMapBlockChange[index] = pos.getY();
-+ }
-+ }
-+
-+ // note: light sets are delayed while processing skylight source changes due to how
-+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
-+ // below nibbles are initialised they aren't reading from partially modified nibbles
-+
-+ // now we can recalculate the sources for the changed columns
-+ for (int index = 0; index < (16 * 16); ++index) {
-+ final int maxY = this.heightMapBlockChange[index];
-+ if (maxY == Integer.MIN_VALUE) {
-+ // not changed
-+ continue;
-+ }
-+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
-+
-+ final int columnX = (index & 15) | (chunkX << 4);
-+ final int columnZ = (index >>> 4) | (chunkZ << 4);
-+
-+ // try and propagate from the above y
-+ // delay light set until after processing all sources to setup
-+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
-+
-+ // maxPropagationY is now the highest block that could not be propagated to
-+
-+ // remove all sources below that are 15
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
-+
-+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
-+ if ((currY & 15) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
-+ }
-+
-+ // ensure section below is always checked
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
-+ if (nibble == null) {
-+ // advance currY to the the top of the section below
-+ currY = (currY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+ continue;
-+ }
-+
-+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
-+ break;
-+ }
-+
-+ // delay light set until after processing all sources to setup
-+ this.appendToDecreaseQueue(
-+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // do not set transparent blocks for the same reason we don't in the checkBlock method
-+ );
-+ }
-+ }
-+ }
-+
-+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
-+ // immediate light value
-+ this.processDelayedIncreases();
-+ this.processDelayedDecreases();
-+
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected final int[] heightMapGen = new int[32 * 32];
-+
-+ @Override
-+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ int highestNonEmptySection = this.maxSection;
-+ while (highestNonEmptySection == (this.minSection - 1) ||
-+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isEmpty()) {
-+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
-+ // try propagate FULL to neighbours
-+
-+ // check neighbours to see if we need to propagate into them
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourX = chunkX + direction.x;
-+ final int neighbourZ = chunkZ + direction.z;
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
-+ if (neighbourNibble == null) {
-+ // unloaded neighbour
-+ // most of the time we fall here
-+ continue;
-+ }
-+
-+ // it looks like we need to propagate into the neighbour
-+
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (direction.x != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (direction.z < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
-+
-+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
-+ );
-+ }
-+ }
-+ }
-+
-+ if (highestNonEmptySection-- == (this.minSection - 1)) {
-+ break;
-+ }
-+ }
-+
-+ if (highestNonEmptySection >= this.minSection) {
-+ // fill out our other sources
-+ final int minX = chunkPos.x << 4;
-+ final int maxX = chunkPos.x << 4 | 15;
-+ final int minZ = chunkPos.z << 4;
-+ final int maxZ = chunkPos.z << 4 | 15;
-+ final int startY = highestNonEmptySection << 4 | 15;
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
-+ }
-+ }
-+ } // else: apparently the chunk is empty
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ // no need to rewrite the nibble cache again
-+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+ } else {
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+
-+ protected final void processDelayedIncreases() {
-+ // copied from performLightIncrease
-+ final long[] queue = this.increaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+ }
-+
-+ protected final void processDelayedDecreases() {
-+ // copied from performLightDecrease
-+ final long[] queue = this.decreaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+
-+ this.setLightLevel(posX, posY, posZ, 0);
-+ }
-+ }
-+
-+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
-+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
-+ // clobbering the light values will result in broken propagation)
-+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
-+ final boolean extrudeInitialised, final boolean delayLightSet) {
-+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
-+
-+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
-+ return startY;
-+ }
-+
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+
-+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
-+ if (above == null) {
-+ above = AIR_BLOCK_STATE;
-+ }
-+
-+ for (;startY >= (this.minLightSection << 4); --startY) {
-+ if ((startY & 15) == 15) {
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+ }
-+ BlockState current = this.getBlockState(worldX, startY, worldZ);
-+ if (current == null) {
-+ current = AIR_BLOCK_STATE;
-+ }
-+
-+ final VoxelShape fromShape;
-+ if (above.isConditionallyFullOpaque()) {
-+ this.mutablePos2.set(worldX, startY + 1, worldZ);
-+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ // above wont let us propagate
-+ break;
-+ }
-+ } else {
-+ fromShape = Shapes.empty();
-+ }
-+
-+ final int opacityIfCached = current.getOpacityIfCached();
-+ // does light propagate from the top down?
-+ if (opacityIfCached != -1) {
-+ if (opacityIfCached != 0) {
-+ // we cannot propagate 15 through this
-+ break;
-+ }
-+ // most of the time it falls here.
-+ // add to propagate
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ );
-+ } else {
-+ mutablePos.set(worldX, startY, worldZ);
-+ long flags = 0L;
-+ if (current.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ // can't propagate here, we're done on this column.
-+ break;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = current.getLightBlock(world, mutablePos);
-+ if (opacity > 0) {
-+ // let the queued value (if any) handle it from here.
-+ break;
-+ }
-+
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | flags
-+ );
-+ }
-+
-+ above = current;
-+
-+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
-+ // we skip empty sections here, as this is just an easy way of making sure the above block
-+ // can propagate through air.
-+
-+ // nothing can propagate in null sections, remove the queue entry for it
-+ --this.increaseQueueInitialLength;
-+
-+ // advance currY to the the top of the section below
-+ startY = (startY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+
-+ // make sure this is marked as AIR
-+ above = AIR_BLOCK_STATE;
-+ } else if (!delayLightSet) {
-+ this.setLightLevel(worldX, startY, worldZ, 15);
-+ }
-+ }
-+
-+ return startY;
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ef930395407533a2c669499c02989bbbe0ac6101
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java
-@@ -0,0 +1,1573 @@
-+package ca.spottedleaf.starlight.light;
-+
-+import com.tuinity.tuinity.util.CoordinateUtils;
-+import com.tuinity.tuinity.util.IntegerUtil;
-+import com.tuinity.tuinity.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.world.level.*;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public abstract class StarLightEngine {
-+
-+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
-+
-+ protected static final LevelChunkSection EMPTY_CHUNK_SECTION = new LevelChunkSection(0);
-+
-+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
-+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
-+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
-+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
-+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
-+ };
-+
-+ protected static enum AxisDirection {
-+
-+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
-+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
-+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
-+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
-+
-+ static {
-+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
-+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
-+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
-+ }
-+
-+ protected AxisDirection opposite;
-+
-+ public final int x;
-+ public final int y;
-+ public final int z;
-+ public final Direction nms;
-+ public final long everythingButThisDirection;
-+ public final long everythingButTheOppositeDirection;
-+
-+ AxisDirection(final int x, final int y, final int z) {
-+ this.x = x;
-+ this.y = y;
-+ this.z = z;
-+ this.nms = Direction.fromNormal(x, y, z);
-+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
-+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
-+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
-+ }
-+
-+ public AxisDirection getOpposite() {
-+ return this.opposite;
-+ }
-+ }
-+
-+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
-+ // for explaining how light propagates via breadth-first search
-+
-+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
-+ // exactly how the vanilla light engine should behave for minecraft.
-+
-+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
-+ protected final LevelChunkSection[] sectionCache;
-+
-+ // the exact same as above, except for storing fast access to SWMRNibbleArray
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final SWMRNibbleArray[] nibbleCache;
-+
-+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final boolean[] notifyUpdateCache;
-+
-+ // always initialsed during start of lighting.
-+ // index = x + (z * 5)
-+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
-+
-+ // index = x + (z * 5)
-+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
-+
-+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
-+
-+ protected int encodeOffsetX;
-+ protected int encodeOffsetY;
-+ protected int encodeOffsetZ;
-+
-+ protected int coordinateOffset;
-+
-+ protected int chunkOffsetX;
-+ protected int chunkOffsetY;
-+ protected int chunkOffsetZ;
-+
-+ protected int chunkIndexOffset;
-+ protected int chunkSectionIndexOffset;
-+
-+ protected final boolean skylightPropagator;
-+ protected final int emittedLightMask;
-+ protected final boolean isClientSide;
-+
-+ protected final Level world;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+ protected final int minSection;
-+ protected final int maxSection;
-+
-+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
-+ this.skylightPropagator = skylightPropagator;
-+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
-+ this.isClientSide = world.isClientSide;
-+ this.world = world;
-+ this.minLightSection = WorldUtil.getMinLightSection(world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+
-+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ }
-+
-+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
-+ // 31 = center + encodeOffset
-+ this.encodeOffsetX = 31 - centerX;
-+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
-+ this.encodeOffsetZ = 31 - centerZ;
-+
-+ // coordinateIndex = x | (z << 6) | (y << 12)
-+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
-+
-+ // 2 = (centerX >> 4) + chunkOffset
-+ this.chunkOffsetX = 2 - (centerX >> 4);
-+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
-+ this.chunkOffsetZ = 2 - (centerZ >> 4);
-+
-+ // chunk index = x + (5 * z)
-+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
-+
-+ // chunk section index = x + (5 * z) + ((5*5) * y)
-+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
-+ }
-+
-+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
-+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
-+ final int centerChunkX = centerX >> 4;
-+ final int centerChunkY = centerY >> 4;
-+ final int centerChunkZ = centerZ >> 4;
-+
-+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
-+
-+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
-+
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ final int cx = centerChunkX + dx;
-+ final int cz = centerChunkZ + dz;
-+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
-+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
-+
-+ if (chunk == null) {
-+ if (relaxed | isTwoRadius) {
-+ continue;
-+ }
-+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
-+ }
-+
-+ if (!this.canUseChunk(chunk)) {
-+ continue;
-+ }
-+
-+ this.setChunkInCache(cx, cz, chunk);
-+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
-+ if (!isTwoRadius) {
-+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
-+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
-+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
-+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
-+ }
-+
-+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
-+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
-+ }
-+
-+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
-+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? (sections[cy - this.minSection] == null || sections[cy - this.minSection].isEmpty() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION));
-+ }
-+ }
-+
-+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
-+
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
-+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
-+ }
-+
-+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
-+ }
-+ }
-+
-+ protected final void updateVisible(final LightChunkGetter lightAccess) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
-+ continue;
-+ }
-+
-+ final int chunkX = (index % 5) - this.chunkOffsetX;
-+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
-+ final int chunkY = ((index / (5*5)) % (16 + 2 + 2)) - this.chunkOffsetY;
-+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
-+ }
-+ }
-+ }
-+
-+ protected final void destroyCaches() {
-+ Arrays.fill(this.sectionCache, null);
-+ Arrays.fill(this.nibbleCache, null);
-+ Arrays.fill(this.chunkCache, null);
-+ Arrays.fill(this.emptinessMapCache, null);
-+ if (this.isClientSide) {
-+ Arrays.fill(this.notifyUpdateCache, false);
-+ }
-+ }
-+
-+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
-+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ if (section != null) {
-+ return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
-+ }
-+
-+ return null;
-+ }
-+
-+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
-+ final LevelChunkSection section = this.sectionCache[sectionIndex];
-+
-+ if (section != null) {
-+ return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.states.get(localIndex);
-+ }
-+
-+ return null;
-+ }
-+
-+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
-+ }
-+
-+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
-+ }
-+
-+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
-+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set(localIndex, level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
-+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
-+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
-+ }
-+
-+ protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) {
-+ throw new UnsupportedOperationException(); // :(
-+ }
-+
-+ // warn: localIndex = y | (x << 4) | (z << 8)
-+ protected final long getKnownTransparency(final int sectionIndex, final int localIndex) {
-+ throw new UnsupportedOperationException(); // :(
-+ }
-+
-+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
-+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
-+ }
-+
-+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
-+
-+ for (int i = 0, len = ret.length; i < len; ++i) {
-+ ret[i] = new SWMRNibbleArray(null, true);
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
-+
-+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
-+
-+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
-+
-+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
-+
-+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
-+
-+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Set positions, final Boolean[] changedSections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ if (changedSections != null) {
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ }
-+ if (!positions.isEmpty()) {
-+ this.propagateBlockChanges(lightAccess, chunk, positions);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions);
-+
-+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
-+
-+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
-+ // if ret == expect, then expect is the correct light value for pos
-+ // if ret < expect, then ret is the real light value
-+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect);
-+
-+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
-+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
-+
-+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (currNibble == null) {
-+ return;
-+ }
-+
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ chunkY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null) {
-+ continue;
-+ }
-+
-+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
-+ // both are zero, nothing to check.
-+ continue;
-+ }
-+
-+ // this chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ int centerDelayedChecks = 0;
-+ int neighbourDelayedChecks = 0;
-+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int neighbourX = currX + neighbourOffX;
-+ final int neighbourZ = currZ + neighbourOffZ;
-+
-+ final int currentIndex = (currX & 15) |
-+ ((currZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int currentLevel = currNibble.getUpdating(currentIndex);
-+
-+ final int neighbourIndex =
-+ (neighbourX & 15) |
-+ ((neighbourZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
-+
-+ // the checks are delayed because the checkBlock method clobbers light values - which then
-+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
-+ // way, they do have a negative performance impact due to simply queueing more values
-+
-+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
-+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
-+ }
-+
-+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
-+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
-+ }
-+ }
-+ }
-+
-+ final int currentChunkOffX = chunkX << 4;
-+ final int currentChunkOffZ = chunkZ << 4;
-+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
-+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
-+ final int chunkOffY = chunkY << 4;
-+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
-+ // try to queue neighbouring data together
-+ // index = x | (z << 4) | (y << 8)
-+ if (i < centerDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
-+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ currentChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ if (i < neighbourDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
-+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
-+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
-+ // This does not resolve skylight source problems.
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
-+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
-+ if (currNibble == null) {
-+ continue;
-+ }
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ currSectionY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
-+ // can't pull from 0
-+ continue;
-+ }
-+
-+ // neighbour chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = (chunkX << 4) - 1;
-+ } else {
-+ startX = (chunkX << 4) + 16;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = (chunkZ << 4) - 1;
-+ } else {
-+ startZ = (chunkZ << 4) + 16;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int level = neighbourNibble.getUpdating(
-+ (currX & 15)
-+ | ((currZ & 15) << 4)
-+ | ((currY & 15) << 8)
-+ );
-+
-+ if (level <= 1) {
-+ // nothing to propagate
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((level & 0xFL) << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
-+ );
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ final Boolean[] ret = new Boolean[sections.length];
-+
-+ for (int i = 0; i < sections.length; ++i) {
-+ if (sections[i] == null || sections[i].isEmpty()) {
-+ ret[i] = Boolean.TRUE;
-+ } else {
-+ ret[i] = Boolean.FALSE;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Boolean[] emptinessChanges) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
-+
-+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // subclasses are guaranteed that this is always called before a changed block set
-+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
-+ // rets non-null when the emptiness map changed and needs to be updated
-+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final Boolean[] emptinessChanges, final boolean unlit) {
-+ final Level world = (Level)lightAccess.getLevel();
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+
-+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+ boolean[] ret = null;
-+ final boolean needsInit = unlit || chunkEmptinessMap == null;
-+ if (needsInit) {
-+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
-+ }
-+
-+ // update emptiness map
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ if (valueBoxed == null) {
-+ if (needsInit) {
-+ throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?");
-+ }
-+ continue;
-+ }
-+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
-+ }
-+
-+ // now init neighbour nibbles
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ final int sectionY = sectionIndex + this.minSection;
-+ if (valueBoxed == null) {
-+ continue;
-+ }
-+
-+ final boolean empty = valueBoxed.booleanValue();
-+
-+ if (empty) {
-+ continue;
-+ }
-+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // if we're not empty, we also need to initialise nibbles
-+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ for (int dy = 1; dy >= -1; --dy) {
-+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ // check for de-init and lazy-init
-+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
-+ // init checks.
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // does this neighbour have 1 radius loaded?
-+ boolean neighboursLoaded = true;
-+ neighbour_loaded_search:
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
-+ neighboursLoaded = false;
-+ break neighbour_loaded_search;
-+ }
-+ }
-+ }
-+
-+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
-+ // check neighbours to see if we need to de-init this one
-+ boolean allEmpty = true;
-+ neighbour_search:
-+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int y = sectionY + dy2;
-+ if (y < this.minSection || y > this.maxSection) {
-+ // empty
-+ continue;
-+ }
-+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
-+ if (emptinessMap != null) {
-+ if (!emptinessMap[y - this.minSection]) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ } else {
-+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
-+ if (section != null && section != EMPTY_CHUNK_SECTION) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ if (allEmpty & neighboursLoaded) {
-+ // can only de-init when neighbours are loaded
-+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
-+ // to be correct
-+
-+ // all were empty, so de-init
-+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
-+ } else if (!allEmpty) {
-+ // must init
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, sections);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
-+ // chunks light values with respect to neighbours
-+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
-+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
-+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
-+
-+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+
-+ try {
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.lightChunk(lightAccess, chunk, true);
-+ this.setNibbles(chunk, nibbles);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void relightChunks(final LightChunkGetter lightAccess, final Set chunks,
-+ final Consumer chunkLightCallback, final IntConsumer onComplete) {
-+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
-+ // the region of chunks to relight
-+ // it's required that tickets are added for each chunk to keep them loaded
-+ final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
-+
-+ final int[] neighbourLightOrder = new int[] {
-+ // d = 0
-+ 0, 0,
-+ // d = 1
-+ -1, 0,
-+ 0, -1,
-+ 1, 0,
-+ 0, 1,
-+ // d = 2
-+ -1, 1,
-+ 1, 1,
-+ -1, -1,
-+ 1, -1,
-+ };
-+
-+ int lightCalls = 0;
-+
-+ for (final ChunkPos chunkPos : chunks) {
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
-+ if (chunk == null || !this.canUseChunk(chunk)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
-+ final int dx = neighbourLightOrder[i];
-+ final int dz = neighbourLightOrder[i + 1];
-+ final int neighbourX = dx + chunkX;
-+ final int neighbourZ = dz + chunkZ;
-+
-+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
-+ if (neighbour == null || !this.canUseChunk(neighbour)) {
-+ continue;
-+ }
-+
-+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
-+ // lit already called for neighbour, no need to light it now
-+ continue;
-+ }
-+
-+ // light neighbour chunk
-+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
-+ try {
-+ // insert all neighbouring chunks for this neighbour that we have data for
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int neighbourX2 = neighbourX + dx2;
-+ final int neighbourZ2 = neighbourZ + dz2;
-+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
-+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
-+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
-+ if (nibbles == null) {
-+ // we haven't lit this chunk
-+ continue;
-+ }
-+
-+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
-+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
-+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
-+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
-+ }
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ // now insert the neighbour chunk and light it
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
-+ nibblesByChunk.put(key, nibbles);
-+
-+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
-+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
-+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
-+
-+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
-+ emptinessMapByChunk.put(key, neighbourEmptiness);
-+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
-+ this.setEmptinessMap(neighbour, neighbourEmptiness);
-+ }
-+
-+ this.lightChunk(lightAccess, neighbour, false);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // done lighting all neighbours, so the chunk is now fully lit
-+
-+ // make sure nibbles are fully updated before calling back
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ for (final SWMRNibbleArray nibble : nibbles) {
-+ nibble.updateVisible();
-+ }
-+
-+ this.setNibbles(chunk, nibbles);
-+
-+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX));
-+ }
-+
-+ // now do callback
-+ if (chunkLightCallback != null) {
-+ chunkLightCallback.accept(chunkPos);
-+ }
-+ ++lightCalls;
-+ }
-+
-+ if (onComplete != null) {
-+ onComplete.accept(lightCalls);
-+ }
-+ }
-+
-+ // old algorithm for propagating
-+ // this is also the basic algorithm, the optimised algorithm is always going to be tested against this one
-+ // and this one is always tested against vanilla
-+ // contains:
-+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
-+ // next 4 bits: propagated light level (0, 15]
-+ // next 6 bits: propagation direction bitset
-+ // next 24 bits: unused
-+ // last 4 bits: state flags
-+ // state flags:
-+ // whether the propagation must set the current position's light value (0 if decrease, propagated light level if increase)
-+ // whether the propagation needs to check if its current level is equal to the expected level
-+ // used only in increase propagation
-+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
-+ // whether the propagation needs to consider if its block is conditionally transparent
-+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
-+
-+ protected long[] increaseQueue = new long[16 * 16 * 16];
-+ protected int increaseQueueInitialLength;
-+ protected long[] decreaseQueue = new long[16 * 16 * 16];
-+ protected int decreaseQueueInitialLength;
-+
-+ protected final long[] resizeIncreaseQueue() {
-+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
-+ }
-+
-+ protected final long[] resizeDecreaseQueue() {
-+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
-+ }
-+
-+ protected final void appendToIncreaseQueue(final long value) {
-+ final int idx = this.increaseQueueInitialLength++;
-+ long[] queue = this.increaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected final void appendToDecreaseQueue(final long value) {
-+ final int idx = this.decreaseQueueInitialLength++;
-+ long[] queue = this.decreaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
-+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
-+ static {
-+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
-+ final List directions = new ArrayList<>();
-+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
-+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
-+ }
-+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
-+ }
-+ }
-+
-+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.increaseQueueInitialLength;
-+ this.increaseQueueInitialLength = 0;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
-+
-+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
-+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
-+ // not at the level we expect, so something changed.
-+ continue;
-+ }
-+ }
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want or unloaded
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.decreaseQueue;
-+ long[] increaseQueue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.decreaseQueueInitialLength;
-+ this.decreaseQueueInitialLength = 0;
-+ int increaseQueueLength = this.increaseQueueInitialLength;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
-+ }
-+
-+ currentNibble.set(localIndex, emittedLight);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+
-+ currentNibble.set(localIndex, emittedLight);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
-+ }
-+
-+ currentNibble.set(localIndex, emittedLight);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+
-+ currentNibble.set(localIndex, emittedLight);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ // propagate sources we clobbered
-+ this.increaseQueueInitialLength = increaseQueueLength;
-+ this.performLightIncrease(lightAccess);
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..300364f693583be802a71d94cda5d96c77c7b67c
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java
-@@ -0,0 +1,635 @@
-+package ca.spottedleaf.starlight.light;
-+
-+import com.tuinity.tuinity.util.CoordinateUtils;
-+import com.tuinity.tuinity.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ChunkStatus;
-+import net.minecraft.world.level.chunk.DataLayer;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import net.minecraft.world.level.lighting.LevelLightEngine;
-+import java.util.*;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public final class StarLightInterface {
-+
-+ public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong()));
-+
-+ /**
-+ * Can be {@code null}, indicating the light is all empty.
-+ */
-+ protected final Level world;
-+ protected final LightChunkGetter lightAccess;
-+
-+ protected final ArrayDeque cachedSkyPropagators;
-+ protected final ArrayDeque cachedBlockPropagators;
-+
-+ protected final LightQueue lightQueue = new LightQueue(this);
-+
-+ protected final LayerLightEventListener skyReader;
-+ protected final LayerLightEventListener blockReader;
-+ protected final boolean isClientSide;
-+
-+ protected final int minSection;
-+ protected final int maxSection;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+
-+ public final LevelLightEngine lightEngine;
-+
-+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
-+ this.lightAccess = lightAccess;
-+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
-+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.isClientSide = !(this.world instanceof ServerLevel);
-+ if (this.world == null) {
-+ this.minSection = 0;
-+ this.maxSection = 15;
-+ this.minLightSection = -1;
-+ this.maxLightSection = 16;
-+ } else {
-+ this.minSection = WorldUtil.getMinSection(this.world);
-+ this.maxSection = WorldUtil.getMaxSection(this.world);
-+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
-+ }
-+ this.lightEngine = lightEngine;
-+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) {
-+ // skylight doesn't care
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runUpdates(final int i, final boolean bl, final boolean bl2) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void enableLightSources(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return null;
-+ }
-+
-+ final int sectionY = pos.getY();
-+
-+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
-+ return null;
-+ }
-+
-+ if (chunk.getSkyEmptinessMap() == null) {
-+ return null;
-+ }
-+
-+ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ final int x = blockPos.getX();
-+ int y = blockPos.getY();
-+ final int z = blockPos.getZ();
-+
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4);
-+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return 15;
-+ }
-+
-+ int sectionY = y >> 4;
-+
-+ if (sectionY > StarLightInterface.this.maxLightSection) {
-+ return 15;
-+ }
-+
-+ if (sectionY < StarLightInterface.this.minLightSection) {
-+ sectionY = StarLightInterface.this.minLightSection;
-+ y = sectionY << 4;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles();
-+ final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection];
-+
-+ if (StarLightInterface.this.isClientSide) {
-+ if (!immediate.isNullNibbleUpdating()) {
-+ return immediate.getUpdating(x, y, z);
-+ }
-+ } else {
-+ if (!immediate.isNullNibbleVisible()) {
-+ return immediate.getVisible(x, y, z);
-+ }
-+ }
-+
-+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap();
-+
-+ if (emptinessMap == null) {
-+ return 15;
-+ }
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = StarLightInterface.this.minLightSection - 1;
-+ for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) {
-+ if (emptinessMap[currY - StarLightInterface.this.minSection]) {
-+ continue;
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (sectionY > lowestY) {
-+ return 15;
-+ }
-+
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = sectionY + 1; currY <= StarLightInterface.this.maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection];
-+ if (StarLightInterface.this.isClientSide) {
-+ if (!nibble.isNullNibbleUpdating()) {
-+ return nibble.getUpdating(x, 0, z);
-+ }
-+ } else {
-+ if (!nibble.isNullNibbleVisible()) {
-+ return nibble.getVisible(x, 0, z);
-+ }
-+ }
-+ }
-+
-+ // should never reach here
-+ return 15;
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) {
-+ this.checkBlock(blockPos);
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runUpdates(final int i, final boolean bl, final boolean bl2) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void enableLightSources(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+
-+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
-+ return null;
-+ }
-+
-+ return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ final int cx = blockPos.getX() >> 4;
-+ final int cy = blockPos.getY() >> 4;
-+ final int cz = blockPos.getZ() >> 4;
-+
-+ if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) {
-+ return 0;
-+ }
-+
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz);
-+
-+ if (chunk == null) {
-+ return 0;
-+ }
-+
-+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection];
-+ if (StarLightInterface.this.isClientSide) {
-+ return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ());
-+ } else {
-+ return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ());
-+ }
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ }
-+
-+ public LayerLightEventListener getSkyReader() {
-+ return this.skyReader;
-+ }
-+
-+ public LayerLightEventListener getBlockReader() {
-+ return this.blockReader;
-+ }
-+
-+ public boolean isClientSide() {
-+ return this.isClientSide;
-+ }
-+
-+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
-+ if (this.world == null) {
-+ // empty world
-+ return null;
-+ }
-+ return ((ServerLevel)this.world).getChunkSource().getChunkAtImmediately(chunkX, chunkZ);
-+ }
-+
-+ public boolean hasUpdates() {
-+ return !this.lightQueue.isEmpty();
-+ }
-+
-+ public Level getWorld() {
-+ return this.world;
-+ }
-+
-+ public LightChunkGetter getLightAccess() {
-+ return this.lightAccess;
-+ }
-+
-+ protected final SkyStarLightEngine getSkyLightEngine() {
-+ if (this.cachedSkyPropagators == null) {
-+ return null;
-+ }
-+ final SkyStarLightEngine ret;
-+ synchronized (this.cachedSkyPropagators) {
-+ ret = this.cachedSkyPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new SkyStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) {
-+ if (this.cachedSkyPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedSkyPropagators) {
-+ this.cachedSkyPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ protected final BlockStarLightEngine getBlockLightEngine() {
-+ if (this.cachedBlockPropagators == null) {
-+ return null;
-+ }
-+ final BlockStarLightEngine ret;
-+ synchronized (this.cachedBlockPropagators) {
-+ ret = this.cachedBlockPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new BlockStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) {
-+ if (this.cachedBlockPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedBlockPropagators) {
-+ this.cachedBlockPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ public CompletableFuture blockChange(final BlockPos pos) {
-+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueBlockChange(pos);
-+ }
-+
-+ public CompletableFuture sectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ if (this.world == null) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
-+ }
-+
-+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void relightChunks(final Set chunks, final Consumer chunkLightCallback,
-+ final IntConsumer onComplete) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
-+ blockEngine == null ? onComplete : null);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
-+ this.checkSkyEdges(chunkX, chunkZ);
-+ this.checkBlockEdges(chunkX, chunkZ);
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void scheduleChunkLight(final ChunkPos pos, final Runnable run) {
-+ this.lightQueue.queueChunkLighting(pos, run);
-+ }
-+
-+ public void removeChunkTasks(final ChunkPos pos) {
-+ this.lightQueue.removeChunk(pos);
-+ }
-+
-+ public void propagateChanges() {
-+ if (this.lightQueue.isEmpty()) {
-+ return;
-+ }
-+
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ LightQueue.ChunkTasks task;
-+ while ((task = this.lightQueue.removeFirstTask()) != null) {
-+ if (task.lightTasks != null) {
-+ for (final Runnable run : task.lightTasks) {
-+ run.run();
-+ }
-+ }
-+
-+ final long coordinate = task.chunkCoordinate;
-+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
-+
-+ final Set positions = task.changedPositions;
-+ final Boolean[] sectionChanges = task.changedSectionSet;
-+
-+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+
-+ if (skyEngine != null && task.queuedEdgeChecksSky != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky);
-+ }
-+ if (blockEngine != null && task.queuedEdgeChecksBlock != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock);
-+ }
-+
-+ task.onComplete.complete(null);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ protected static final class LightQueue {
-+
-+ protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
-+ protected final StarLightInterface manager;
-+
-+ public LightQueue(final StarLightInterface manager) {
-+ this.manager = manager;
-+ }
-+
-+ public synchronized boolean isEmpty() {
-+ return this.chunkTasks.isEmpty();
-+ }
-+
-+ public synchronized CompletableFuture queueBlockChange(final BlockPos pos) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ tasks.changedPositions.add(pos.immutable());
-+ return tasks.onComplete;
-+ }
-+
-+ public synchronized CompletableFuture queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ if (tasks.changedSectionSet == null) {
-+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1];
-+ }
-+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue);
-+
-+ return tasks.onComplete;
-+ }
-+
-+ public synchronized CompletableFuture queueChunkLighting(final ChunkPos pos, final Runnable lightTask) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ if (tasks.lightTasks == null) {
-+ tasks.lightTasks = new ArrayList<>();
-+ }
-+ tasks.lightTasks.add(lightTask);
-+
-+ return tasks.onComplete;
-+ }
-+
-+ public synchronized CompletableFuture queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks.onComplete;
-+ }
-+
-+ public synchronized CompletableFuture queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks.onComplete;
-+ }
-+
-+ public void removeChunk(final ChunkPos pos) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos));
-+ }
-+ if (tasks != null) {
-+ tasks.onComplete.complete(null);
-+ }
-+ }
-+
-+ public synchronized ChunkTasks removeFirstTask() {
-+ if (this.chunkTasks.isEmpty()) {
-+ return null;
-+ }
-+ return this.chunkTasks.removeFirst();
-+ }
-+
-+ protected static final class ChunkTasks {
-+
-+ public final Set changedPositions = new HashSet<>();
-+ public Boolean[] changedSectionSet;
-+ public ShortOpenHashSet queuedEdgeChecksSky;
-+ public ShortOpenHashSet queuedEdgeChecksBlock;
-+ public List lightTasks;
-+
-+ public final CompletableFuture onComplete = new CompletableFuture<>();
-+
-+ public final long chunkCoordinate;
-+
-+ public ChunkTasks(final long chunkCoordinate) {
-+ this.chunkCoordinate = chunkCoordinate;
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java
-index b9cdbf8acccfd6b207a0116f068168f3b8c8e17d..7404989c37ee1b7aa4e6999a063180d099532f7e 100644
---- a/src/main/java/co/aikar/timings/MinecraftTimings.java
-+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java
-@@ -45,6 +45,8 @@ public final class MinecraftTimings {
-
- public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update");
- public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate");
-+ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager
-+ public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search
-
- private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap();
-
-diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
-index 2ff4d4921e2076abf415bd3c8f5173ecd6222168..9d920565ff65a84b1b9a2a4777fd8bc8f07e0153 100644
---- a/src/main/java/co/aikar/timings/TimingsExport.java
-+++ b/src/main/java/co/aikar/timings/TimingsExport.java
-@@ -153,7 +153,7 @@ public class TimingsExport extends Thread {
- return pair(rule, world.getWorld().getGameRuleValue(rule));
- })),
- pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance()),
-- pair("notick-viewdistance", world.getChunkSource().chunkMap.getEffectiveNoTickViewDistance())
-+ pair("notick-viewdistance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()) // Tuinity - replace old player chunk management
- ));
- }));
-
-@@ -228,7 +228,8 @@ public class TimingsExport extends Thread {
- parent.put("config", createObject(
- pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)),
- pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)),
-- pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null))
-+ pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Tuinity - add config to timings report
-+ pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)) // Tuinity - add config to timings report
- ));
-
- new TimingsExport(listeners, parent, history).start();
-diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java
-index 218f5bafeed8551b55b91c7fccaf6935c8b631ca..3918b24c98faa5232c7ffd733ba8000562132785 100644
---- a/src/main/java/com/destroystokyo/paper/Metrics.java
-+++ b/src/main/java/com/destroystokyo/paper/Metrics.java
-@@ -593,7 +593,7 @@ public class Metrics {
- boolean logFailedRequests = config.getBoolean("logFailedRequests", false);
- // Only start Metrics, if it's enabled in the config
- if (config.getBoolean("enabled", true)) {
-- Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger());
-+ Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page
-
- metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> {
- String minecraftVersion = Bukkit.getVersion();
-@@ -603,7 +603,7 @@ public class Metrics {
-
- metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size()));
- metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() ? "online" : "offline"));
-- metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown"));
-+ metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Tuinity - we have our own bstats page
-
- metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
- Map> map = new HashMap<>();
-diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
-index ba8395435482fc4d6e02fc86794cdb0d35d4399c..af66c6d863a57b2c34006b06852e9811a74d7dfa 100644
---- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
-+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
-@@ -272,7 +272,7 @@ public class PaperCommand extends Command {
- int ticking = 0;
- int entityTicking = 0;
-
-- for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
-+ for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunks.getUpdatingMap().values()) { // Tuinity - change updating chunks map
- if (chunk.getFullChunkUnchecked() == null) {
- continue;
- }
-@@ -496,6 +496,46 @@ public class PaperCommand extends Command {
- }
- }
-
-+ // Tuinity start - rewrite light engine
-+ private void starlightFixLight(ServerPlayer sender, ServerLevel world, ThreadedLevelLightEngine lightengine, int radius) {
-+ long start = System.nanoTime();
-+ java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
-+
-+ int[] pending = new int[1];
-+ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final net.minecraft.world.level.chunk.ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ ++pending[0];
-+ }
-+
-+ int[] relitChunks = new int[1];
-+ lightengine.relight(chunks,
-+ (ChunkPos chunkPos) -> {
-+ ++relitChunks[0];
-+ sender.getBukkitEntity().sendMessage(
-+ ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE +
-+ ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%"
-+ );
-+ },
-+ (int totalRelit) -> {
-+ final long end = System.nanoTime();
-+ final long diff = Math.round(1.0e-6*(end - start));
-+ sender.getBukkitEntity().sendMessage(
-+ ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " +
-+ ChatColor.DARK_AQUA + diff + "ms"
-+ );
-+ });
-+ sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks");
-+ }
-+ // Tuinity end - rewrite light engine
-+
- private void doFixLight(CommandSender sender, String[] args) {
- if (!(sender instanceof Player)) {
- sender.sendMessage("Only players can use this command");
-@@ -504,7 +544,7 @@ public class PaperCommand extends Command {
- int radius = 2;
- if (args.length > 1) {
- try {
-- radius = Math.min(5, Integer.parseInt(args[1]));
-+ radius = Math.min(32, Integer.parseInt(args[1])); // Tuinity - MOOOOOORE
- } catch (Exception e) {
- sender.sendMessage("Not a number");
- return;
-@@ -517,6 +557,13 @@ public class PaperCommand extends Command {
- ServerLevel world = (ServerLevel) handle.level;
- ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
-
-+ // Tuinity start - rewrite light engine
-+ if (true) {
-+ this.starlightFixLight(handle, world, lightengine, radius);
-+ return;
-+ }
-+ // Tuinity end - rewrite light engine
-+
- net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation());
- Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius));
- updateLight(sender, world, lightengine, queue);
-diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
-index 580bae0d414d371a07a6bfeefc41fdd989dc0083..d50b61876f15d95b836b3dd81d9c3492c91a8448 100644
---- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
-+++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
-@@ -29,8 +29,8 @@ public class PaperVersionFetcher implements VersionFetcher {
- @Nonnull
- @Override
- public Component getVersionMessage(@Nonnull String serverVersion) {
-- String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]");
-- final Component updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]);
-+ String[] parts = serverVersion.substring("git-Tuinity-".length()).split("[-\\s]"); // Tuinity
-+ final Component updateMessage = getUpdateStatusMessage("Spottedleaf/Tuinity", GITHUB_BRANCH_NAME, parts[0]); // Tuinity
- final Component history = getHistory();
-
- return history != null ? TextComponent.ofChildren(updateMessage, Component.newline(), history) : updateMessage;
-@@ -54,13 +54,10 @@ public class PaperVersionFetcher implements VersionFetcher {
-
- private static Component getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) {
- int distance;
-- try {
-- int jenkinsBuild = Integer.parseInt(versionInfo);
-- distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion());
-- } catch (NumberFormatException ignored) {
-+ // Tuinity - we don't have jenkins setup
- versionInfo = versionInfo.replace("\"", "");
- distance = fetchDistanceFromGitHub(repo, branch, versionInfo);
-- }
-+ // Tuinity - we don't have jenkins setup
-
- switch (distance) {
- case -1:
-diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
-index da13ff17609b7bc8076d9297edf8decf01a2ed88..b4c69d39eee19339b1de295151d7ed3bf61635c1 100644
---- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
-+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
-@@ -312,6 +312,7 @@ public final class PaperTickList extends ServerTickList { // extend to avo
- toTick.tickState = STATE_SCHEDULED;
- this.addToNotTickingReady(toTick);
- }
-+ MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick
- } catch (final Throwable thr) {
- // start copy from TickListServer // TODO check on update
- CrashReport crashreport = CrashReport.forThrowable(thr, "Exception while ticking");
-diff --git a/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d9bfe05d4f86229ed743113bfb0bbd983adb7e68
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/chunk/PlayerChunkLoader.java
-@@ -0,0 +1,965 @@
-+package com.tuinity.tuinity.chunk;
-+
-+import com.destroystokyo.paper.util.misc.PlayerAreaMap;
-+import com.destroystokyo.paper.util.misc.PooledLinkedHashSets;
-+import com.tuinity.tuinity.config.TuinityConfig;
-+import com.tuinity.tuinity.util.CoordinateUtils;
-+import com.tuinity.tuinity.util.TickThread;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
-+import net.minecraft.network.protocol.Packet;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
-+import net.minecraft.server.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.TreeSet;
-+import java.util.concurrent.atomic.AtomicInteger;
-+
-+public final class PlayerChunkLoader {
-+
-+ public static final int MIN_VIEW_DISTANCE = 2;
-+ public static final int MAX_VIEW_DISTANCE = 32;
-+
-+ public static final int TICK_TICKET_LEVEL = 31;
-+ public static final int LOADED_TICKET_LEVEL = 33;
-+
-+ protected final ChunkMap chunkMap;
-+ protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f);
-+ protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f);
-+
-+ protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> {
-+ if (p1 == p2) {
-+ return 0;
-+ }
-+
-+ final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst();
-+ final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst();
-+
-+ final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority);
-+
-+ if (priorityCompare != 0) {
-+ return priorityCompare;
-+ }
-+
-+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId());
-+
-+ if (idCompare != 0) {
-+ return idCompare;
-+ }
-+
-+ // last resort
-+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2));
-+ });
-+
-+ protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> {
-+ if (p1 == p2) {
-+ return 0;
-+ }
-+
-+ final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget);
-+ if (timeCompare != 0) {
-+ return timeCompare;
-+ }
-+
-+ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId());
-+
-+ if (idCompare != 0) {
-+ return idCompare;
-+ }
-+
-+ // last resort
-+ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2));
-+ });
-+
-+
-+ // no throttling is applied below this VD for loading
-+
-+ /**
-+ * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded.
-+ */
-+ public final PlayerAreaMap broadcastMap;
-+
-+ /**
-+ * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded.
-+ */
-+ public final PlayerAreaMap loadMap;
-+
-+ /**
-+ * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus,
-+ * this map is always representing the chunks we are actually going to load.
-+ */
-+ public final PlayerAreaMap loadTicketCleanup;
-+
-+ /**
-+ * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen.
-+ */
-+ public final PlayerAreaMap tickMap;
-+
-+ /**
-+ * -1 if defaulting to [load distance], else always in [2, load distance]
-+ */
-+ protected int rawSendDistance = -1;
-+
-+ /**
-+ * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1]
-+ */
-+ protected int rawLoadDistance = -1;
-+
-+ /**
-+ * Never -1, always in [2, 32]
-+ */
-+ protected int rawTickDistance = -1;
-+
-+ // methods to bridge for API
-+
-+ public int getTargetViewDistance() {
-+ return this.getTickDistance();
-+ }
-+
-+ public void setTargetViewDistance(final int distance) {
-+ this.setTickDistance(distance);
-+ }
-+
-+ public int getTargetNoTickViewDistance() {
-+ return this.getLoadDistance() - 1;
-+ }
-+
-+ public void setTargetNoTickViewDistance(final int distance) {
-+ this.setLoadDistance(distance == -1 ? -1 : distance + 1);
-+ }
-+
-+ public int getTargetSendDistance() {
-+ return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance;
-+ }
-+
-+ public void setTargetSendDistance(final int distance) {
-+ this.setSendDistance(distance);
-+ }
-+
-+ // internal methods
-+
-+ public int getSendDistance() {
-+ final int loadDistance = this.getLoadDistance();
-+ return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance);
-+ }
-+
-+ public void setSendDistance(final int distance) {
-+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.rawSendDistance = distance;
-+ }
-+
-+ public int getLoadDistance() {
-+ final int tickDistance = this.getTickDistance();
-+ return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance);
-+ }
-+
-+ public void setLoadDistance(final int distance) {
-+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.rawLoadDistance = distance;
-+ }
-+
-+ public int getTickDistance() {
-+ return this.rawTickDistance;
-+ }
-+
-+ public void setTickDistance(final int distance) {
-+ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.rawTickDistance = distance;
-+ }
-+
-+ /*
-+ Players have 3 different types of view distance:
-+ 1. Sending view distance
-+ 2. Loading view distance
-+ 3. Ticking view distance
-+
-+ But for configuration purposes (and API) there are:
-+ 1. No-tick view distance
-+ 2. Tick view distance
-+ 3. Broadcast view distance
-+
-+ These aren't always the same as the types we represent internally.
-+
-+ Loading view distance is always max(no-tick + 1, tick + 1)
-+ - no-tick has 1 added because clients need an extra radius to render chunks
-+ - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking
-+
-+ Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means
-+ it loads chunks in radius load-view-distance + 1.
-+
-+ The maximum value for send view distance is the load view distance. API can set it lower.
-+ */
-+
-+ public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) {
-+ this.chunkMap = chunkMap;
-+ this.broadcastMap = new PlayerAreaMap(pooledHashSets,
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ if (player.needsChunkCenterUpdate) {
-+ player.needsChunkCenterUpdate = false;
-+ player.connection.send(new ClientboundSetChunkCacheCenterPacket(currPosX, currPosZ));
-+ }
-+ PlayerChunkLoader.this.onChunkEnter(player, rangeX, rangeZ);
-+ },
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ);
-+ });
-+ this.loadMap = new PlayerAreaMap(pooledHashSets,
-+ null,
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ if (newState != null) {
-+ return;
-+ }
-+ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ));
-+ });
-+ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets,
-+ null,
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ if (newState != null) {
-+ return;
-+ }
-+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
-+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
-+ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) {
-+ --PlayerChunkLoader.this.concurrentChunkLoads;
-+ }
-+ });
-+ this.tickMap = new PlayerAreaMap(pooledHashSets,
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ if (newState.size() != 1) {
-+ return;
-+ }
-+ LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ);
-+ if (chunk == null || !chunk.areNeighboursLoaded(2)) {
-+ return;
-+ }
-+
-+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
-+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
-+ },
-+ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
-+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
-+ if (newState != null) {
-+ return;
-+ }
-+ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
-+ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
-+ });
-+ }
-+
-+ protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet();
-+ protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet();
-+
-+ // rets whether the chunk is at a loaded stage that is ready to be sent to players
-+ public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) {
-+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key);
-+
-+ if (chunk == null) {
-+ return false;
-+ }
-+
-+ return chunk.getSendingChunk() != null && this.isTargetedForPlayerLoad.contains(key);
-+ }
-+
-+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerLoaderData data = this.playerMap.get(player);
-+ if (data == null) {
-+ return false;
-+ }
-+
-+ return data.hasSentChunk(chunkX, chunkZ);
-+ }
-+
-+ protected int getMaxConcurrentChunkSends() {
-+ double config = TuinityConfig.playerMaxConcurrentChunkSends;
-+ return Math.max(1, config <= 0 ? (int)Math.ceil(-config * this.chunkMap.level.players().size()) : (int)config);
-+ }
-+
-+ protected int getMaxChunkLoads() {
-+ double config = TuinityConfig.playerMaxConcurrentChunkLoads;
-+ return Math.max(1, (config <= 0 ? (int)Math.ceil(-config * MinecraftServer.getServer().getPlayerCount()) : (int)config) * 9);
-+ }
-+
-+ protected double getTargetSendRatePerPlayer() {
-+ double config = TuinityConfig.playerTargetChunkSendRate;
-+ return config <= 0 ? -config : config / MinecraftServer.getServer().getPlayerCount();
-+ }
-+
-+ public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) {
-+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
-+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
-+ }
-+
-+ public void onChunkSendReady(final int chunkX, final int chunkZ) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ);
-+
-+ if (playersInSendRange == null) {
-+ return;
-+ }
-+
-+ final Object[] rawData = playersInSendRange.getBackingSet();
-+ for (int i = 0, len = rawData.length; i < len; ++i) {
-+ final Object raw = rawData[i];
-+
-+ if (!(raw instanceof ServerPlayer)) {
-+ continue;
-+ }
-+ this.onChunkEnter((ServerPlayer)raw, chunkX, chunkZ);
-+ }
-+
-+ // now let's try and queue mid tick logic again
-+ }
-+
-+ public void onChunkEnter(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerLoaderData data = this.playerMap.get(player);
-+
-+ if (data == null) {
-+ return;
-+ }
-+
-+ if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) {
-+ // if we don't have player tickets, then the load logic will pick this up and queue to send
-+ return;
-+ }
-+
-+ final long playerPos = this.broadcastMap.getLastCoordinate(player);
-+ final int playerChunkX = CoordinateUtils.getChunkX(playerPos);
-+ final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos);
-+ final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ);
-+
-+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0);
-+ data.sendQueue.add(holder);
-+ }
-+
-+ public void onChunkLoad(final int chunkX, final int chunkZ) {
-+ if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ --this.concurrentChunkLoads;
-+ }
-+ }
-+
-+ public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerLoaderData data = this.playerMap.get(player);
-+
-+ if (data == null) {
-+ return;
-+ }
-+
-+ data.unloadChunk(chunkX, chunkZ);
-+ }
-+
-+ public void addPlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread("Cannot add player async");
-+ if (!player.isRealPlayer) {
-+ return;
-+ }
-+ final PlayerLoaderData data = new PlayerLoaderData(player, this);
-+ if (this.playerMap.putIfAbsent(player, data) == null) {
-+ data.update();
-+ }
-+ }
-+
-+ public void removePlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread("Cannot remove player async");
-+ if (!player.isRealPlayer) {
-+ return;
-+ }
-+
-+ final PlayerLoaderData loaderData = this.playerMap.remove(player);
-+ if (loaderData == null) {
-+ return;
-+ }
-+ loaderData.remove();
-+ this.chunkLoadQueue.remove(loaderData);
-+ this.chunkSendQueue.remove(loaderData);
-+ this.chunkSendWaitQueue.remove(loaderData);
-+ synchronized (this.sendingChunkCounts) {
-+ final int count = this.sendingChunkCounts.removeInt(loaderData);
-+ if (count != 0) {
-+ concurrentChunkSends.getAndAdd(-count);
-+ }
-+ }
-+ }
-+
-+ public void updatePlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread("Cannot update player async");
-+ if (!player.isRealPlayer) {
-+ return;
-+ }
-+ final PlayerLoaderData loaderData = this.playerMap.get(player);
-+ if (loaderData != null) {
-+ loaderData.update();
-+ }
-+ }
-+
-+ public PlayerLoaderData getData(final ServerPlayer player) {
-+ return this.playerMap.get(player);
-+ }
-+
-+ public void tick() {
-+ TickThread.ensureTickThread("Cannot tick async");
-+ for (final PlayerLoaderData data : this.playerMap.values()) {
-+ data.update();
-+ }
-+ this.tickMidTick();
-+ }
-+
-+ protected static final AtomicInteger concurrentChunkSends = new AtomicInteger();
-+ protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>();
-+ private void trySendChunks() {
-+ final long time = System.nanoTime();
-+ // drain entries from wait queue
-+ while (!this.chunkSendWaitQueue.isEmpty()) {
-+ final PlayerLoaderData data = this.chunkSendWaitQueue.first();
-+
-+ if (data.nextChunkSendTarget > time) {
-+ break;
-+ }
-+
-+ this.chunkSendWaitQueue.pollFirst();
-+
-+ this.chunkSendQueue.add(data);
-+ }
-+
-+ if (this.chunkSendQueue.isEmpty()) {
-+ return;
-+ }
-+
-+ final int maxSends = this.getMaxConcurrentChunkSends();
-+ final double sendRate = this.getTargetSendRatePerPlayer();
-+ final long nextDeadline = (long)((1 / sendRate) * 1.0e9) + time;
-+ for (;;) {
-+ if (this.chunkSendQueue.isEmpty()) {
-+ break;
-+ }
-+ final int currSends = concurrentChunkSends.get();
-+ if (currSends >= maxSends) {
-+ break;
-+ }
-+
-+ if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) {
-+ continue;
-+ }
-+
-+ // send chunk
-+
-+ final PlayerLoaderData data = this.chunkSendQueue.removeFirst();
-+
-+ final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst();
-+ if (queuedSend == null) {
-+ concurrentChunkSends.getAndDecrement(); // we never sent, so decrease
-+ // stop iterating over players who have nothing to send
-+ if (this.chunkSendQueue.isEmpty()) {
-+ // nothing left
-+ break;
-+ }
-+ continue;
-+ }
-+
-+ if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ data.nextChunkSendTarget = nextDeadline;
-+ this.chunkSendWaitQueue.add(data);
-+
-+ synchronized (this.sendingChunkCounts) {
-+ this.sendingChunkCounts.addTo(data, 1);
-+ }
-+
-+ data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> {
-+ synchronized (this.sendingChunkCounts) {
-+ final int count = this.sendingChunkCounts.getInt(data);
-+ if (count == 0) {
-+ // disconnected, so we don't need to decrement: it will be decremented for us
-+ return;
-+ }
-+ if (count == 1) {
-+ this.sendingChunkCounts.removeInt(data);
-+ } else {
-+ this.sendingChunkCounts.put(data, count - 1);
-+ }
-+ }
-+
-+ concurrentChunkSends.getAndDecrement();
-+ });
-+ }
-+ }
-+
-+ protected int concurrentChunkLoads;
-+ private void tryLoadChunks() {
-+ if (this.chunkLoadQueue.isEmpty()) {
-+ return;
-+ }
-+
-+ final int maxLoads = this.getMaxChunkLoads();
-+ for (;;) {
-+ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst();
-+
-+ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst();
-+ if (queuedLoad == null) {
-+ if (this.chunkLoadQueue.isEmpty()) {
-+ break;
-+ }
-+ continue;
-+ }
-+
-+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) {
-+ // already loaded!
-+ data.loadQueue.pollFirst(); // already loaded so we just skip
-+ this.chunkLoadQueue.add(data);
-+
-+ // ensure the chunk is queued to send
-+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ);
-+ continue;
-+ }
-+
-+ final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ);
-+
-+ final double priority = queuedLoad.priority;
-+ // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present.
-+ // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the
-+ // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they
-+ // aren't required, it bypasses the limiter system.
-+ boolean unloadedTargetChunk = false;
-+ unloaded_check:
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final int offX = queuedLoad.chunkX + dx;
-+ final int offZ = queuedLoad.chunkZ + dz;
-+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) {
-+ unloadedTargetChunk = true;
-+ break unloaded_check;
-+ }
-+ }
-+ }
-+ if (unloadedTargetChunk && priority > 0.0) {
-+ // priority > 0.0 implies rate limited chunks
-+
-+ final int currentChunkLoads = this.concurrentChunkLoads;
-+ if (currentChunkLoads >= maxLoads) {
-+ // don't poll, we didn't load it
-+ this.chunkLoadQueue.add(data);
-+ break;
-+ }
-+ }
-+
-+ // can only poll after we decide to load
-+ data.loadQueue.pollFirst();
-+
-+ // now that we've polled we can re-add to load queue
-+ this.chunkLoadQueue.add(data);
-+
-+ // add necessary tickets to load chunk up to send-ready
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final int offX = queuedLoad.chunkX + dx;
-+ final int offZ = queuedLoad.chunkZ + dz;
-+ final ChunkPos chunkPos = new ChunkPos(offX, offZ);
-+
-+ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
-+ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) {
-+ continue;
-+ }
-+
-+ if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) {
-+ // wont reach here if unloadedTargetChunk is false
-+ ++this.concurrentChunkLoads;
-+ }
-+ }
-+ }
-+
-+ // mark that we've added tickets here
-+ this.isTargetedForPlayerLoad.add(chunkKey);
-+
-+ // it's possible all we needed was the player tickets to queue up the send.
-+ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) {
-+ // yup, all we needed.
-+ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ);
-+ }
-+ }
-+ }
-+
-+ public void tickMidTick() {
-+ // try to send more chunks
-+ this.trySendChunks();
-+
-+ // try to queue more chunks to load
-+ this.tryLoadChunks();
-+ }
-+
-+ static final class ChunkPriorityHolder {
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final int manhattanDistanceToPlayer;
-+ public final double priority;
-+
-+ public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) {
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.manhattanDistanceToPlayer = manhattanDistanceToPlayer;
-+ this.priority = priority;
-+ }
-+ }
-+
-+ public static final class PlayerLoaderData {
-+
-+ protected static final float FOV = 110.0f;
-+ protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0;
-+
-+ // Player max sprint speed is approximately 8m/s
-+ protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0);
-+ protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f;
-+
-+ protected double lastLocX = Double.NEGATIVE_INFINITY;
-+ protected double lastLocZ = Double.NEGATIVE_INFINITY;
-+
-+ protected int lastChunkX;
-+ protected int lastChunkZ;
-+
-+ // this is corrected so that 0 is along the positive x-axis
-+ protected float lastYaw = Float.NEGATIVE_INFINITY;
-+
-+ protected int lastSendDistance = Integer.MIN_VALUE;
-+ protected int lastLoadDistance = Integer.MIN_VALUE;
-+ protected int lastTickDistance = Integer.MIN_VALUE;
-+ protected boolean usingLookingPriority;
-+
-+ protected final ServerPlayer player;
-+ protected final PlayerChunkLoader loader;
-+
-+ // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field
-+ // in a comparator!
-+ protected final ArrayDeque loadQueue = new ArrayDeque<>();
-+ protected final LongOpenHashSet sentChunks = new LongOpenHashSet();
-+
-+ protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> {
-+ final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer);
-+ if (distanceCompare != 0) {
-+ return distanceCompare;
-+ }
-+
-+ final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX);
-+ if (coordinateXCompare != 0) {
-+ return coordinateXCompare;
-+ }
-+
-+ return Integer.compare(p1.chunkZ, p2.chunkZ);
-+ });
-+
-+ protected int sendViewDistance = -1;
-+ protected int loadViewDistance = -1;
-+ protected int tickViewDistance = -1;
-+
-+ protected long nextChunkSendTarget;
-+
-+ public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) {
-+ this.player = player;
-+ this.loader = loader;
-+ }
-+
-+ // these view distance methods are for api
-+ public int getTargetSendViewDistance() {
-+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
-+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
-+ final int clientViewDistance = this.getClientViewDistance();
-+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!TuinityConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance);
-+ return sendViewDistance;
-+ }
-+
-+ public void setTargetSendViewDistance(final int distance) {
-+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.sendViewDistance = distance;
-+ }
-+
-+ public int getTargetNoTickViewDistance() {
-+ return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1;
-+ }
-+
-+ public void setTargetNoTickViewDistance(final int distance) {
-+ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.loadViewDistance = distance == -1 ? -1 : distance + 1;
-+ }
-+
-+ public int getTargetTickViewDistance() {
-+ return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
-+ }
-+
-+ public void setTargetTickViewDistance(final int distance) {
-+ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ this.tickViewDistance = distance;
-+ }
-+
-+ protected int getLoadDistance() {
-+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
-+
-+ return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
-+ }
-+
-+ public boolean hasSentChunk(final int chunkX, final int chunkZ) {
-+ return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) {
-+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player,
-+ new ChunkPos(chunkX, chunkZ), new Packet[2], false, true); // unloaded, loaded
-+ this.player.connection.connection.execute(onChunkSend);
-+ } else {
-+ throw new IllegalStateException();
-+ }
-+ }
-+
-+ public void unloadChunk(final int chunkX, final int chunkZ) {
-+ if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player,
-+ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded
-+ }
-+ }
-+
-+ protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point
-+ final double p2x, final double p2z, // triangle point
-+ final double p3x, final double p3z, // triangle point
-+
-+ final double targetX, final double targetZ) { // point
-+ // from barycentric coordinates:
-+ // targetX = a*p1x + b*p2x + c*p3x
-+ // targetZ = a*p1z + b*p2z + c*p3z
-+ // 1.0 = a*1.0 + b*1.0 + c*1.0
-+ // where a, b, c >= 0.0
-+ // so, if any of a, b, c are less-than zero then there is no intersection.
-+
-+ // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z))
-+ // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d
-+ // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d
-+ // c = 1.0 - a - b
-+
-+ final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z);
-+ final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d;
-+
-+ if (a < 0.0 || a > 1.0) {
-+ return false;
-+ }
-+
-+ final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d;
-+ if (b < 0.0 || b > 1.0) {
-+ return false;
-+ }
-+
-+ final double c = 1.0 - a - b;
-+
-+ return c >= 0.0 && c <= 1.0;
-+ }
-+
-+ public void remove() {
-+ this.loader.broadcastMap.remove(this.player);
-+ this.loader.loadMap.remove(this.player);
-+ this.loader.loadTicketCleanup.remove(this.player);
-+ this.loader.tickMap.remove(this.player);
-+ }
-+
-+ protected int getClientViewDistance() {
-+ return this.player.clientViewDistance == null ? -1 : this.player.clientViewDistance.intValue();
-+ }
-+
-+ public void update() {
-+ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
-+ // load view cannot be less-than tick view + 1
-+ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
-+ // send view cannot be greater-than load view
-+ final int clientViewDistance = this.getClientViewDistance();
-+ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!TuinityConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance);
-+
-+ final double posX = this.player.getX();
-+ final double posZ = this.player.getZ();
-+ final float yaw = MCUtil.normalizeYaw(this.player.yRot + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis
-+
-+ // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them.
-+ final boolean useLookPriority = TuinityConfig.playerFrustumPrioritisation && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD ||
-+ this.player.getAbilities().flying);
-+
-+ // make sure we're in the send queue
-+ this.loader.chunkSendWaitQueue.add(this);
-+
-+ if (
-+ // has view distance stayed the same?
-+ sendViewDistance == this.lastSendDistance
-+ && loadViewDistance == this.lastLoadDistance
-+ && tickViewDistance == this.lastTickDistance
-+
-+ && (this.usingLookingPriority ? (
-+ // has our block stayed the same (this also accounts for chunk change)?
-+ Mth.floor(this.lastLocX) == Mth.floor(posX)
-+ && Mth.floor(this.lastLocZ) == Mth.floor(posZ)
-+ ) : (
-+ // has our chunk stayed the same
-+ (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4)
-+ && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4)
-+ ))
-+
-+ // has our decision about look priority changed?
-+ && this.usingLookingPriority == useLookPriority
-+
-+ // if we are currently using look priority, has our yaw stayed within recalc threshold?
-+ && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD)
-+ ) {
-+ // nothing we care about changed, so we're not re-calculating
-+ return;
-+ }
-+
-+ final int centerChunkX = Mth.floor(posX) >> 4;
-+ final int centerChunkZ = Mth.floor(posZ) >> 4;
-+
-+ this.player.needsChunkCenterUpdate = true;
-+ this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance);
-+ this.player.needsChunkCenterUpdate = false;
-+ this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance);
-+ this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1);
-+ this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance);
-+
-+ if (sendViewDistance != this.lastSendDistance) {
-+ // update the view radius for client
-+ // note that this should be after the map calls because the client wont expect unload calls not in its VD
-+ // and it's possible we decreased VD here
-+ this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance - 1)); // client already expects the 1 radius neighbours, so subtract 1.
-+ }
-+
-+ this.lastLocX = posX;
-+ this.lastLocZ = posZ;
-+ this.lastYaw = yaw;
-+ this.lastSendDistance = sendViewDistance;
-+ this.lastLoadDistance = loadViewDistance;
-+ this.lastTickDistance = tickViewDistance;
-+ this.usingLookingPriority = useLookPriority;
-+
-+ this.lastChunkX = centerChunkX;
-+ this.lastChunkZ = centerChunkZ;
-+
-+ // points for player "view" triangle:
-+
-+ // obviously, the player pos is a vertex
-+ final double p1x = posX;
-+ final double p1z = posZ;
-+
-+ // to the left of the looking direction
-+ final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector
-+ + p1x; // offset vector
-+ final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector
-+ + p1z; // offset vector
-+
-+ // to the right of the looking direction
-+ final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector
-+ + p1x; // offset vector
-+ final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector
-+ + p1z; // offset vector
-+
-+ // now that we have all of our points, we can recalculate the load queue
-+
-+ final List loadQueue = new ArrayList<>();
-+
-+ // clear send queue, we are re-sorting
-+ this.sendQueue.clear();
-+
-+ final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance);
-+
-+ for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) {
-+ for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) {
-+ final int chunkX = dx + centerChunkX;
-+ final int chunkZ = dz + centerChunkZ;
-+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
-+
-+ if (this.hasSentChunk(chunkX, chunkZ)) {
-+ // already sent (which means it is also loaded)
-+ continue;
-+ }
-+
-+ final boolean loadChunk = squareDistance <= loadViewDistance;
-+ final boolean sendChunk = squareDistance <= sendViewDistance;
-+
-+ final boolean prioritised = useLookPriority && triangleIntersects(
-+ // prioritisation triangle
-+ p1x, p1z, p2x, p2z, p3x, p3z,
-+
-+ // center of chunk
-+ (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8)
-+ );
-+
-+
-+ final int manhattanDistance = (Math.abs(dx) + Math.abs(dz));
-+
-+ final double priority;
-+
-+ if (squareDistance <= TuinityConfig.playerMinChunkLoadRadius) {
-+ // priority should be negative, and we also want to order it from center outwards
-+ // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest
-+ priority = -((2 * TuinityConfig.playerMinChunkLoadRadius + 1) - (dx + dz));
-+ } else {
-+ if (prioritised) {
-+ // we don't prioritise these chunks above others because we also want to make sure some chunks
-+ // will be loaded if the player changes direction
-+ priority = (double)manhattanDistance / 6.0;
-+ } else {
-+ priority = (double)manhattanDistance;
-+ }
-+ }
-+
-+ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority);
-+
-+ if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) {
-+ if (loadChunk) {
-+ loadQueue.add(holder);
-+ }
-+ } else {
-+ // loaded but not sent: so queue it!
-+ if (sendChunk) {
-+ this.sendQueue.add(holder);
-+ }
-+ }
-+ }
-+ }
-+
-+ loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> {
-+ return Double.compare(p1.priority, p2.priority);
-+ });
-+
-+ // we're modifying loadQueue, must remove
-+ this.loader.chunkLoadQueue.remove(this);
-+
-+ this.loadQueue.clear();
-+ this.loadQueue.addAll(loadQueue);
-+
-+ // must re-add
-+ this.loader.chunkLoadQueue.add(this);
-+ }
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..49dc783a312ed62415d28cdd801dad6a96f3cc16
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java
-@@ -0,0 +1,477 @@
-+package com.tuinity.tuinity.chunk;
-+
-+import com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.server.MCUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.function.Supplier;
-+
-+public final class SingleThreadChunkRegionManager {
-+
-+ protected final int regionSectionMergeRadius;
-+ protected final int regionSectionChunkSize;
-+ public final int regionChunkShift; // log2(REGION_CHUNK_SIZE)
-+
-+ public final ServerLevel world;
-+ public final String name;
-+
-+ protected final Long2ObjectOpenHashMap regionsBySection = new Long2ObjectOpenHashMap<>();
-+ protected final ReferenceLinkedOpenHashSet needsRecalculation = new ReferenceLinkedOpenHashSet<>();
-+ protected final int minSectionRecalcCount;
-+ protected final double maxDeadRegionPercent;
-+ protected final Supplier regionDataSupplier;
-+ protected final Supplier regionSectionDataSupplier;
-+
-+ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount,
-+ final double maxDeadRegionPercent, final int sectionMergeRadius,
-+ final int regionSectionChunkShift,
-+ final String name, final Supplier regionDataSupplier,
-+ final Supplier regionSectionDataSupplier) {
-+ this.regionSectionMergeRadius = sectionMergeRadius;
-+ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
-+ this.regionChunkShift = regionSectionChunkShift;
-+ this.world = world;
-+ this.name = name;
-+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
-+ this.maxDeadRegionPercent = maxDeadRegionPercent;
-+ this.regionDataSupplier = regionDataSupplier;
-+ this.regionSectionDataSupplier = regionSectionDataSupplier;
-+ }
-+
-+ // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f
-+
-+ /*
-+ protected void check() {
-+ ReferenceOpenHashSet> checked = new ReferenceOpenHashSet<>();
-+
-+ for (RegionSection section : this.regionsBySection.values()) {
-+ if (!checked.add(section.region)) {
-+ section.region.check();
-+ }
-+ }
-+ for (Region region : this.needsRecalculation) {
-+ region.check();
-+ }
-+ }
-+ */
-+
-+ protected void addToRecalcQueue(final Region region) {
-+ this.needsRecalculation.add(region);
-+ }
-+
-+ protected void removeFromRecalcQueue(final Region region) {
-+ this.needsRecalculation.remove(region);
-+ }
-+
-+ public RegionSection getRegionSection(final int chunkX, final int chunkZ) {
-+ return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift));
-+ }
-+
-+ public Region getRegion(final int chunkX, final int chunkZ) {
-+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift));
-+ return section != null ? section.region : null;
-+ }
-+
-+ private final List toMerge = new ArrayList<>();
-+
-+ protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) {
-+ final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ);
-+
-+ if (force == null) {
-+ RegionSection region = this.regionsBySection.get(sectionKey);
-+ if (region != null) {
-+ return region;
-+ }
-+ }
-+
-+ int mergeCandidateSectionSize = -1;
-+ Region mergeIntoCandidate = null;
-+
-+ // find optimal candidate to merge into
-+
-+ final int minX = sectionX - this.regionSectionMergeRadius;
-+ final int maxX = sectionX + this.regionSectionMergeRadius;
-+ final int minZ = sectionZ - this.regionSectionMergeRadius;
-+ final int maxZ = sectionZ + this.regionSectionMergeRadius;
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ));
-+ if (section == null) {
-+ continue;
-+ }
-+ final Region region = section.region;
-+ if (region.dead) {
-+ throw new IllegalStateException("Dead region should not be in live region manager state: " + region);
-+ }
-+ final int sections = region.sections.size();
-+
-+ if (sections > mergeCandidateSectionSize) {
-+ mergeCandidateSectionSize = sections;
-+ mergeIntoCandidate = region;
-+ }
-+ this.toMerge.add(region);
-+ }
-+ }
-+
-+ // merge
-+ if (mergeIntoCandidate != null) {
-+ for (int i = 0; i < this.toMerge.size(); ++i) {
-+ final Region region = this.toMerge.get(i);
-+ if (region.dead || mergeIntoCandidate == region) {
-+ continue;
-+ }
-+ region.mergeInto(mergeIntoCandidate);
-+ }
-+ this.toMerge.clear();
-+ } else {
-+ mergeIntoCandidate = new Region(this);
-+ }
-+
-+ final RegionSection section;
-+ if (force == null) {
-+ this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this));
-+ } else {
-+ final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force);
-+ if (existing != null) {
-+ throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() +
-+ ", with " + force.toStringWithRegion());
-+ }
-+
-+ section = force;
-+ }
-+
-+ mergeIntoCandidate.addRegionSection(section);
-+ //mergeIntoCandidate.check();
-+ //this.check();
-+
-+ return section;
-+ }
-+
-+ public void addChunk(final int chunkX, final int chunkZ) {
-+ this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ);
-+ }
-+
-+ public void removeChunk(final int chunkX, final int chunkZ) {
-+ final RegionSection section = this.regionsBySection.get(
-+ MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)
-+ );
-+ if (section != null) {
-+ section.removeChunk(chunkX, chunkZ);
-+ } else {
-+ throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist");
-+ }
-+ }
-+
-+ public void recalculateRegions() {
-+ for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) {
-+ final Region region = this.needsRecalculation.removeFirst();
-+
-+ this.recalculateRegion(region);
-+ //this.check();
-+ }
-+ }
-+
-+ protected void recalculateRegion(final Region region) {
-+ region.markedForRecalc = false;
-+ //region.check();
-+ // clear unused regions
-+ for (final Iterator iterator = region.deadSections.iterator(); iterator.hasNext();) {
-+ final RegionSection deadSection = iterator.next();
-+
-+ if (deadSection.hasChunks()) {
-+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
-+ }
-+ if (!region.removeRegionSection(deadSection)) {
-+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
-+ }
-+ if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) {
-+ throw new IllegalStateException("Cannot remove dead section '" +
-+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
-+ this.regionsBySection.get(deadSection.regionCoordinate));
-+ }
-+ }
-+ region.deadSections.clear();
-+
-+ // implicitly cover cases where size == 0
-+ if (region.sections.size() < this.minSectionRecalcCount) {
-+ //region.check();
-+ return;
-+ }
-+
-+ // run a test to see if we actually need to recalculate
-+ // TODO
-+
-+ // destroy and rebuild the region
-+ region.dead = true;
-+
-+ // destroy region state
-+ for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection aliveSection = iterator.next();
-+ if (!aliveSection.hasChunks()) {
-+ throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!");
-+ }
-+ if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) {
-+ throw new IllegalStateException("Cannot remove alive section '" +
-+ aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
-+ this.regionsBySection.get(aliveSection.regionCoordinate));
-+ }
-+ }
-+
-+ // rebuild regions
-+ for (final Iterator iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection aliveSection = iterator.next();
-+ this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection);
-+ }
-+ }
-+
-+ public static final class Region {
-+ protected final IteratorSafeOrderedReferenceSet sections = new IteratorSafeOrderedReferenceSet<>();
-+ protected final ReferenceOpenHashSet deadSections = new ReferenceOpenHashSet<>(16, 0.7f);
-+ protected boolean dead;
-+ protected boolean markedForRecalc;
-+
-+ public final SingleThreadChunkRegionManager regionManager;
-+ public final RegionData regionData;
-+
-+ protected Region(final SingleThreadChunkRegionManager regionManager) {
-+ this.regionManager = regionManager;
-+ this.regionData = regionManager.regionDataSupplier.get();
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet.Iterator getSections() {
-+ return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS);
-+ }
-+
-+ protected final double getDeadSectionPercent() {
-+ return (double)this.deadSections.size() / (double)this.sections.size();
-+ }
-+
-+ /*
-+ protected void check() {
-+ if (this.dead) {
-+ throw new IllegalStateException("Dead region!");
-+ }
-+ for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection section = iterator.next();
-+ if (section.region != this) {
-+ throw new IllegalStateException("Region section must point to us!");
-+ }
-+ if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) {
-+ throw new IllegalStateException("Region section must match the regionmanager state!");
-+ }
-+ }
-+ }
-+ */
-+
-+ // note: it is not true that the region at this point is not in any region. use the region field on the section
-+ // to see if it is currently in another region.
-+ protected final boolean addRegionSection(final RegionSection section) {
-+ if (!this.sections.add(section)) {
-+ return false;
-+ }
-+
-+ section.sectionData.addToRegion(section, section.region, this);
-+
-+ section.region = this;
-+ return true;
-+ }
-+
-+ protected final boolean removeRegionSection(final RegionSection section) {
-+ if (!this.sections.remove(section)) {
-+ return false;
-+ }
-+
-+ section.sectionData.removeFromRegion(section, this);
-+
-+ return true;
-+ }
-+
-+ protected void mergeInto(final Region mergeTarget) {
-+ if (this == mergeTarget) {
-+ throw new IllegalStateException("Cannot merge a region onto itself");
-+ }
-+ if (this.dead) {
-+ throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget);
-+ } else if (mergeTarget.dead) {
-+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget);
-+ }
-+ this.dead = true;
-+ if (this.markedForRecalc) {
-+ this.regionManager.removeFromRecalcQueue(this);
-+ }
-+
-+ for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection section = iterator.next();
-+
-+ if (!mergeTarget.addRegionSection(section)) {
-+ throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget);
-+ }
-+ }
-+
-+ for (final RegionSection deadSection : this.deadSections) {
-+ if (!this.sections.contains(deadSection)) {
-+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this);
-+ }
-+ mergeTarget.deadSections.add(deadSection);
-+ }
-+ //mergeTarget.check();
-+ }
-+
-+ protected void markSectionAlive(final RegionSection section) {
-+ this.deadSections.remove(section);
-+ if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) {
-+ this.regionManager.removeFromRecalcQueue(this);
-+ this.markedForRecalc = false;
-+ }
-+ }
-+
-+ protected void markSectionDead(final RegionSection section) {
-+ this.deadSections.add(section);
-+ if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) {
-+ this.regionManager.addToRecalcQueue(this);
-+ this.markedForRecalc = true;
-+ }
-+ }
-+
-+ @Override
-+ public String toString() {
-+ final StringBuilder ret = new StringBuilder(128);
-+
-+ ret.append("Region{");
-+ ret.append("dead=").append(this.dead).append(',');
-+ ret.append("markedForRecalc=").append(this.markedForRecalc).append(',');
-+
-+ ret.append("sectionCount=").append(this.sections.size()).append(',');
-+ ret.append("sections=[");
-+ for (final Iterator iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection section = iterator.next();
-+ ret.append(section);
-+ if (iterator.hasNext()) {
-+ ret.append(',');
-+ }
-+ }
-+ ret.append(']');
-+
-+ ret.append('}');
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static final class RegionSection {
-+ protected final long regionCoordinate;
-+ protected final long[] chunksBitset;
-+ protected int chunkCount;
-+ protected Region region;
-+
-+ public final SingleThreadChunkRegionManager regionManager;
-+ public final RegionSectionData sectionData;
-+
-+ protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) {
-+ this.regionCoordinate = regionCoordinate;
-+ this.regionManager = regionManager;
-+ this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)];
-+ this.sectionData = regionManager.regionSectionDataSupplier.get();
-+ }
-+
-+ public int getSectionX() {
-+ return MCUtil.getCoordinateX(this.regionCoordinate);
-+ }
-+
-+ public int getSectionZ() {
-+ return MCUtil.getCoordinateZ(this.regionCoordinate);
-+ }
-+
-+ public Region getRegion() {
-+ return this.region;
-+ }
-+
-+ private int getChunkIndex(final int chunkX, final int chunkZ) {
-+ return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift);
-+ }
-+
-+ protected boolean hasChunks() {
-+ return this.chunkCount != 0;
-+ }
-+
-+ protected void addChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1)));
-+ if (after == bitset) {
-+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ if (++this.chunkCount != 1) {
-+ return;
-+ }
-+ this.region.markSectionAlive(this);
-+ }
-+
-+ protected void removeChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1)));
-+ if (before == bitset) {
-+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ if (--this.chunkCount != 0) {
-+ return;
-+ }
-+ this.region.markSectionDead(this);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "RegionSection{" +
-+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "hash=" + this.hashCode() +
-+ "}";
-+ }
-+
-+ public String toStringWithRegion() {
-+ return "RegionSection{" +
-+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "hash=" + this.hashCode() + "," +
-+ "region=" + this.region +
-+ "}";
-+ }
-+
-+ private static String toString(final long[] array) {
-+ final StringBuilder ret = new StringBuilder();
-+ for (final long value : array) {
-+ // zero pad the hex string
-+ final char[] zeros = new char[Long.SIZE / 4];
-+ Arrays.fill(zeros, '0');
-+ final String string = Long.toHexString(value);
-+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length());
-+
-+ ret.append(zeros);
-+ }
-+
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static interface RegionData {
-+
-+ }
-+
-+ public static interface RegionSectionData {
-+
-+ public void removeFromRegion(final RegionSection section, final Region from);
-+
-+ // removal from the old region is handled via removeFromRegion
-+ public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion);
-+
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..356a6118f1b0b091f7527aec747659025562eafc
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
-@@ -0,0 +1,432 @@
-+package com.tuinity.tuinity.config;
-+
-+import com.destroystokyo.paper.util.SneakyThrow;
-+import net.minecraft.server.MinecraftServer;
-+import org.bukkit.Bukkit;
-+import org.bukkit.configuration.ConfigurationSection;
-+import org.bukkit.configuration.file.YamlConfiguration;
-+import java.io.File;
-+import java.lang.reflect.Method;
-+import java.lang.reflect.Modifier;
-+import java.util.List;
-+import java.util.logging.Level;
-+
-+public final class TuinityConfig {
-+
-+ public static final String CONFIG_HEADER = "Configuration file for Tuinity.";
-+ public static final int CURRENT_CONFIG_VERSION = 2;
-+
-+ private static final Object[] EMPTY = new Object[0];
-+
-+ private static File configFile;
-+ public static YamlConfiguration config;
-+ private static int configVersion;
-+ public static boolean createWorldSections = true;
-+
-+ public static void init(final File file) {
-+ // TODO remove this in the future...
-+ final File tuinityConfig = new File(file.getParent(), "tuinity.yml");
-+ if (!tuinityConfig.exists()) {
-+ final File oldConfig = new File(file.getParent(), "concrete.yml");
-+ oldConfig.renameTo(tuinityConfig);
-+ }
-+ TuinityConfig.configFile = file;
-+ final YamlConfiguration config = new YamlConfiguration();
-+ config.options().header(CONFIG_HEADER);
-+ config.options().copyDefaults(true);
-+
-+ if (!file.exists()) {
-+ try {
-+ file.createNewFile();
-+ } catch (final Exception ex) {
-+ Bukkit.getLogger().log(Level.SEVERE, "Failure to create tuinity config", ex);
-+ }
-+ } else {
-+ try {
-+ config.load(file);
-+ } catch (final Exception ex) {
-+ Bukkit.getLogger().log(Level.SEVERE, "Failure to load tuinity config", ex);
-+ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
-+ throw new RuntimeException(ex); // unreachable
-+ }
-+ }
-+
-+ TuinityConfig.load(config);
-+ }
-+
-+ public static void load(final YamlConfiguration config) {
-+ TuinityConfig.config = config;
-+ TuinityConfig.configVersion = TuinityConfig.getInt("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION);
-+ TuinityConfig.set("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION);
-+
-+ for (final Method method : TuinityConfig.class.getDeclaredMethods()) {
-+ if (method.getReturnType() != void.class || method.getParameterCount() != 0 ||
-+ !Modifier.isPrivate(method.getModifiers()) || !Modifier.isStatic(method.getModifiers())) {
-+ continue;
-+ }
-+
-+ try {
-+ method.setAccessible(true);
-+ method.invoke(null, EMPTY);
-+ } catch (final Exception ex) {
-+ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
-+ throw new RuntimeException(ex); // unreachable
-+ }
-+ }
-+
-+ /* We re-save to add new options */
-+ try {
-+ config.save(TuinityConfig.configFile);
-+ } catch (final Exception ex) {
-+ Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex);
-+ }
-+ }
-+
-+ static void set(final String path, final Object value) {
-+ TuinityConfig.config.set(path, value);
-+ }
-+
-+ static boolean getBoolean(final String path, final boolean dfl) {
-+ TuinityConfig.config.addDefault(path, Boolean.valueOf(dfl));
-+ return TuinityConfig.config.getBoolean(path, dfl);
-+ }
-+
-+ static int getInt(final String path, final int dfl) {
-+ TuinityConfig.config.addDefault(path, Integer.valueOf(dfl));
-+ return TuinityConfig.config.getInt(path, dfl);
-+ }
-+
-+ static long getLong(final String path, final long dfl) {
-+ TuinityConfig.config.addDefault(path, Long.valueOf(dfl));
-+ return TuinityConfig.config.getLong(path, dfl);
-+ }
-+
-+ static double getDouble(final String path, final double dfl) {
-+ TuinityConfig.config.addDefault(path, Double.valueOf(dfl));
-+ return TuinityConfig.config.getDouble(path, dfl);
-+ }
-+
-+ static String getString(final String path, final String dfl) {
-+ TuinityConfig.config.addDefault(path, dfl);
-+ return TuinityConfig.config.getString(path, dfl);
-+ }
-+
-+ public static int playerMinChunkLoadRadius;
-+ public static double playerMaxConcurrentChunkSends;
-+ public static double playerMaxConcurrentChunkLoads;
-+ public static boolean playerAutoConfigureSendViewDistance;
-+ public static boolean enableMC162253Workaround;
-+ public static double playerTargetChunkSendRate;
-+ public static boolean playerFrustumPrioritisation;
-+
-+ private static void newPlayerChunkManagement() {
-+ playerMinChunkLoadRadius = TuinityConfig.getInt("player-chunks.min-load-radius", 2);
-+ playerMaxConcurrentChunkSends = TuinityConfig.getDouble("player-chunks.max-concurrent-sends", 5.0);
-+ playerMaxConcurrentChunkLoads = TuinityConfig.getDouble("player-chunks.max-concurrent-loads", -6.0);
-+ playerAutoConfigureSendViewDistance = TuinityConfig.getBoolean("player-chunks.autoconfig-send-distance", true);
-+ // this costs server bandwidth. latest phosphor or starlight on the client fixes mc162253 anyways.
-+ enableMC162253Workaround = TuinityConfig.getBoolean("player-chunks.enable-mc162253-workaround", true);
-+ playerTargetChunkSendRate = TuinityConfig.getDouble("player-chunks.target-chunk-send-rate", -35.0);
-+ playerFrustumPrioritisation = TuinityConfig.getBoolean("player-chunks.enable-frustum-priority", false);
-+ }
-+
-+ public static final class PacketLimit {
-+ public final double packetLimitInterval;
-+ public final double maxPacketRate;
-+ public final ViolateAction violateAction;
-+
-+ public PacketLimit(final double packetLimitInterval, final double maxPacketRate, final ViolateAction violateAction) {
-+ this.packetLimitInterval = packetLimitInterval;
-+ this.maxPacketRate = maxPacketRate;
-+ this.violateAction = violateAction;
-+ }
-+
-+ public static enum ViolateAction {
-+ KICK, DROP;
-+ }
-+ }
-+
-+ public static String kickMessage;
-+ public static PacketLimit allPacketsLimit;
-+ public static java.util.Map>, PacketLimit> packetSpecificLimits = new java.util.HashMap<>();
-+
-+ private static void packetLimiter() {
-+ packetSpecificLimits.clear();
-+ kickMessage = org.bukkit.ChatColor.translateAlternateColorCodes('&', TuinityConfig.getString("packet-limiter.kick-message", "&cSent too many packets"));
-+ allPacketsLimit = new PacketLimit(
-+ TuinityConfig.getDouble("packet-limiter.limits.all.interval", 7.0),
-+ TuinityConfig.getDouble("packet-limiter.limits.all.max-packet-rate", 500.0),
-+ PacketLimit.ViolateAction.KICK
-+ );
-+ if (allPacketsLimit.maxPacketRate <= 0.0 || allPacketsLimit.packetLimitInterval <= 0.0) {
-+ allPacketsLimit = null;
-+ }
-+ final ConfigurationSection section = TuinityConfig.config.getConfigurationSection("packet-limiter.limits");
-+
-+ // add default packets
-+
-+ // auto recipe limiting
-+ TuinityConfig.getDouble("packet-limiter.limits." +
-+ "PacketPlayInAutoRecipe" + ".interval", 4.0);
-+ TuinityConfig.getDouble("packet-limiter.limits." +
-+ "PacketPlayInAutoRecipe" + ".max-packet-rate", 5.0);
-+ TuinityConfig.getString("packet-limiter.limits." +
-+ "PacketPlayInAutoRecipe" + ".action", PacketLimit.ViolateAction.DROP.name());
-+
-+ final String canonicalName = MinecraftServer.class.getCanonicalName();
-+ final String nmsPackage = canonicalName.substring(0, canonicalName.lastIndexOf("."));
-+ for (final String packetClassName : section.getKeys(false)) {
-+ if (packetClassName.equals("all")) {
-+ continue;
-+ }
-+ Class> packetClazz = null;
-+
-+ try {
-+ packetClazz = Class.forName(nmsPackage + "." + packetClassName);
-+ } catch (final ClassNotFoundException ex) {
-+ for (final String subpackage : java.util.Arrays.asList("game", "handshake", "login", "status")) {
-+ try {
-+ packetClazz = Class.forName("net.minecraft.network.protocol." + subpackage + "." + packetClassName);
-+ } catch (final ClassNotFoundException ignore) {}
-+ }
-+ if (packetClazz == null) {
-+ MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml");
-+ continue;
-+ }
-+ }
-+
-+ if (!net.minecraft.network.protocol.Packet.class.isAssignableFrom(packetClazz)) {
-+ MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml");
-+ continue;
-+ }
-+
-+ if (!(section.get(packetClassName.concat(".interval")) instanceof Number) || !(section.get(packetClassName.concat(".max-packet-rate")) instanceof Number)) {
-+ throw new RuntimeException("Packet limit setting " + packetClassName + " is missing interval or max-packet-rate!");
-+ }
-+
-+ final String actionString = section.getString(packetClassName.concat(".action"), "KICK");
-+ PacketLimit.ViolateAction action = PacketLimit.ViolateAction.KICK;
-+ for (PacketLimit.ViolateAction test : PacketLimit.ViolateAction.values()) {
-+ if (actionString.equalsIgnoreCase(test.name())) {
-+ action = test;
-+ break;
-+ }
-+ }
-+
-+ final double interval = section.getDouble(packetClassName.concat(".interval"));
-+ final double rate = section.getDouble(packetClassName.concat(".max-packet-rate"));
-+
-+ if (interval > 0.0 && rate > 0.0) {
-+ packetSpecificLimits.put((Class)packetClazz, new PacketLimit(interval, rate, action));
-+ }
-+ }
-+ }
-+
-+ public static boolean lagCompensateBlockBreaking;
-+
-+ private static void lagCompensateBlockBreaking() {
-+ lagCompensateBlockBreaking = TuinityConfig.getBoolean("lag-compensate-block-breaking", true);
-+ }
-+
-+ public static boolean sendFullPosForHardCollidingEntities;
-+
-+ private static void sendFullPosForHardCollidingEntities() {
-+ sendFullPosForHardCollidingEntities = TuinityConfig.getBoolean("send-full-pos-for-hard-colliding-entities", true);
-+ }
-+
-+ public static final class WorldConfig {
-+
-+ public final String worldName;
-+ public String configPath;
-+ ConfigurationSection worldDefaults;
-+
-+ public WorldConfig(final String worldName) {
-+ this.worldName = worldName;
-+ this.init();
-+ }
-+
-+ public void init() {
-+ this.worldDefaults = TuinityConfig.config.getConfigurationSection("world-settings.default");
-+ if (this.worldDefaults == null) {
-+ this.worldDefaults = TuinityConfig.config.createSection("world-settings.default");
-+ }
-+
-+ String worldSectionPath = TuinityConfig.configVersion < 1 ? this.worldName : "world-settings.".concat(this.worldName);
-+ ConfigurationSection section = TuinityConfig.config.getConfigurationSection(worldSectionPath);
-+ this.configPath = worldSectionPath;
-+ if (TuinityConfig.createWorldSections) {
-+ if (section == null) {
-+ section = TuinityConfig.config.createSection(worldSectionPath);
-+ }
-+ TuinityConfig.config.set(worldSectionPath, section);
-+ }
-+
-+ this.load();
-+ }
-+
-+ public void load() {
-+ for (final Method method : TuinityConfig.WorldConfig.class.getDeclaredMethods()) {
-+ if (method.getReturnType() != void.class || method.getParameterCount() != 0 ||
-+ !Modifier.isPrivate(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) {
-+ continue;
-+ }
-+
-+ try {
-+ method.setAccessible(true);
-+ method.invoke(this, EMPTY);
-+ } catch (final Exception ex) {
-+ SneakyThrow.sneaky(ex); /* Rethrow, this is critical */
-+ throw new RuntimeException(ex); // unreachable
-+ }
-+ }
-+
-+ if (TuinityConfig.configVersion < 1) {
-+ ConfigurationSection oldSection = TuinityConfig.config.getConfigurationSection(this.worldName);
-+ TuinityConfig.config.set("world-settings.".concat(this.worldName), oldSection);
-+ TuinityConfig.config.set(this.worldName, null);
-+ }
-+
-+ /* We re-save to add new options */
-+ try {
-+ TuinityConfig.config.save(TuinityConfig.configFile);
-+ } catch (final Exception ex) {
-+ Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex);
-+ }
-+ }
-+
-+ /**
-+ * update world defaults for the specified path, but also sets this world's config value for the path
-+ * if it exists
-+ */
-+ void set(final String path, final Object val) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.set(path, val);
-+ if (config != null && config.get(path) != null) {
-+ config.set(path, val);
-+ }
-+ }
-+
-+ boolean getBoolean(final String path, final boolean dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, Boolean.valueOf(dfl));
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getBoolean(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getBoolean(path) : config.getBoolean(path, this.worldDefaults.getBoolean(path));
-+ }
-+
-+ boolean getBooleanRaw(final String path, final boolean dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getBoolean(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getBoolean(path, dfl) : config.getBoolean(path, this.worldDefaults.getBoolean(path, dfl));
-+ }
-+
-+ int getInt(final String path, final int dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, Integer.valueOf(dfl));
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getInt(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getInt(path) : config.getInt(path, this.worldDefaults.getInt(path));
-+ }
-+
-+ int getIntRaw(final String path, final int dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getInt(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getInt(path, dfl) : config.getInt(path, this.worldDefaults.getInt(path, dfl));
-+ }
-+
-+ long getLong(final String path, final long dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, Long.valueOf(dfl));
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getLong(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getLong(path) : config.getLong(path, this.worldDefaults.getLong(path));
-+ }
-+
-+ long getLongRaw(final String path, final long dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getLong(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getLong(path, dfl) : config.getLong(path, this.worldDefaults.getLong(path, dfl));
-+ }
-+
-+ double getDouble(final String path, final double dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, Double.valueOf(dfl));
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getDouble(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getDouble(path) : config.getDouble(path, this.worldDefaults.getDouble(path));
-+ }
-+
-+ double getDoubleRaw(final String path, final double dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ if (TuinityConfig.configVersion < 1) {
-+ if (config != null && config.getDouble(path) == dfl) {
-+ config.set(path, null);
-+ }
-+ }
-+ return config == null ? this.worldDefaults.getDouble(path, dfl) : config.getDouble(path, this.worldDefaults.getDouble(path, dfl));
-+ }
-+
-+ String getString(final String path, final String dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, dfl);
-+ return config == null ? this.worldDefaults.getString(path) : config.getString(path, this.worldDefaults.getString(path));
-+ }
-+
-+ String getStringRaw(final String path, final String dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ return config == null ? this.worldDefaults.getString(path, dfl) : config.getString(path, this.worldDefaults.getString(path, dfl));
-+ }
-+
-+ List getList(final String path, final List dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ this.worldDefaults.addDefault(path, dfl);
-+ return config == null ? this.worldDefaults.getList(path) : config.getList(path, this.worldDefaults.getList(path));
-+ }
-+
-+ List getListRaw(final String path, final List dfl) {
-+ final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath);
-+ return config == null ? this.worldDefaults.getList(path, dfl) : config.getList(path, this.worldDefaults.getList(path, dfl));
-+ }
-+
-+ public int spawnLimitMonsters;
-+ public int spawnLimitAnimals;
-+ public int spawnLimitWaterAmbient;
-+ public int spawnLimitWaterAnimals;
-+ public int spawnLimitAmbient;
-+
-+ private void perWorldSpawnLimit() {
-+ final String path = "spawn-limits";
-+
-+ this.spawnLimitMonsters = this.getIntRaw(path + ".monsters", -1);
-+ this.spawnLimitAnimals = this.getIntRaw(path + ".animals", -1);
-+ this.spawnLimitWaterAmbient = this.getIntRaw(path + ".water-ambient", -1);
-+ this.spawnLimitWaterAnimals = this.getIntRaw(path + ".water-animals", -1);
-+ this.spawnLimitAmbient = this.getIntRaw(path + ".ambient", -1);
-+ }
-+ }
-+
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/CachedLists.java b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..01320aea07b51c97ae5f0654b81d2332f545d42e
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
-@@ -0,0 +1,57 @@
-+package com.tuinity.tuinity.util;
-+
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.phys.AABB;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.util.UnsafeList;
-+import java.util.List;
-+
-+public final class CachedLists {
-+
-+ // Tuinity start - optimise collisions
-+ static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024);
-+ static boolean tempCollisionListInUse;
-+
-+ public static UnsafeList getTempCollisionList() {
-+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
-+ return new UnsafeList<>(16);
-+ }
-+ tempCollisionListInUse = true;
-+ return TEMP_COLLISION_LIST;
-+ }
-+
-+ public static void returnTempCollisionList(List list) {
-+ if (list != TEMP_COLLISION_LIST) {
-+ return;
-+ }
-+ ((UnsafeList)list).setSize(0);
-+ tempCollisionListInUse = false;
-+ }
-+
-+ static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024);
-+ static boolean tempGetEntitiesListInUse;
-+
-+ public static UnsafeList getTempGetEntitiesList() {
-+ if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) {
-+ return new UnsafeList<>(16);
-+ }
-+ tempGetEntitiesListInUse = true;
-+ return TEMP_GET_ENTITIES_LIST;
-+ }
-+
-+ public static void returnTempGetEntitiesList(List list) {
-+ if (list != TEMP_GET_ENTITIES_LIST) {
-+ return;
-+ }
-+ ((UnsafeList)list).setSize(0);
-+ tempGetEntitiesListInUse = false;
-+ }
-+ // Tuinity end - optimise collisions
-+
-+ public static void reset() {
-+ // Tuinity start - optimise collisions
-+ TEMP_COLLISION_LIST.completeReset();
-+ TEMP_GET_ENTITIES_LIST.completeReset();
-+ // Tuinity end - optimise collisions
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..089d66ce4913e97c5fc79daee0f2fd932664f28f
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/CollisionUtil.java
-@@ -0,0 +1,600 @@
-+package com.tuinity.tuinity.util;
-+
-+import com.tuinity.tuinity.voxel.AABBVoxelShape;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.WorldGenRegion;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.CollisionGetter;
-+import net.minecraft.world.level.EntityGetter;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.border.WorldBorder;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
-+import net.minecraft.world.phys.shapes.ArrayVoxelShape;
-+import net.minecraft.world.phys.shapes.CollisionContext;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.List;
-+import java.util.function.BiPredicate;
-+import java.util.function.Predicate;
-+
-+public final class CollisionUtil {
-+
-+ public static final double COLLISION_EPSILON = 1.0E-7;
-+
-+ public static boolean isEmpty(final AABB aabb) {
-+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON && (aabb.maxY - aabb.minY) < COLLISION_EPSILON && (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (maxX - minX) < COLLISION_EPSILON && (maxY - minY) < COLLISION_EPSILON && (maxZ - minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) {
-+ double x = (double)(chunkX << 4);
-+ double z = (double)(chunkZ << 4);
-+ // use a bounding box bigger than the chunk to prevent entities from entering it on move
-+ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
-+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false);
-+ }
-+
-+ /*
-+ A couple of rules for VoxelShape collisions:
-+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
-+ checks.
-+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
-+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
-+ will automatically round it to 0.
-+ */
-+
-+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
-+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
-+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
-+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
-+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
-+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON &&
-+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON &&
-+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) {
-+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON &&
-+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON &&
-+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON;
-+ }
-+
-+ public static double collideX(final AABB target, final AABB source, final double source_move) {
-+ if (source_move == 0.0) {
-+ return 0.0;
-+ }
-+
-+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ public static double collideY(final AABB target, final AABB source, final double source_move) {
-+ if (source_move == 0.0) {
-+ return 0.0;
-+ }
-+
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ public static double collideZ(final AABB target, final AABB source, final double source_move) {
-+ if (source_move == 0.0) {
-+ return 0.0;
-+ }
-+
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ public static AABB offsetX(final AABB box, final double dx) {
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB offsetY(final AABB box, final double dy) {
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB offsetZ(final AABB box, final double dz) {
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false);
-+ }
-+
-+ public static double performCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideX(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideY(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideZ(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) {
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performCollisionsY(axisalignedbb, y, potentialCollisions);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performCollisionsX(axisalignedbb, x, potentialCollisions);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static boolean addBoxesToIfIntersects(final VoxelShape shape, final AABB aabb, final List list) {
-+ if (shape instanceof AABBVoxelShape) {
-+ final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape;
-+ if (voxelShapeIntersect(shapeCasted.aabb, aabb) && !isEmpty(shapeCasted.aabb)) {
-+ list.add(shapeCasted.aabb);
-+ return true;
-+ }
-+ return false;
-+ } else if (shape instanceof ArrayVoxelShape) {
-+ final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape;
-+ // this can be optimised by checking an "overall shape" first, but not needed
-+
-+ final double offX = shapeCasted.getOffsetX();
-+ final double offY = shapeCasted.getOffsetY();
-+ final double offZ = shapeCasted.getOffsetZ();
-+
-+ boolean ret = false;
-+
-+ for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) {
-+ final double minX, minY, minZ, maxX, maxY, maxZ;
-+ if (voxelShapeIntersect(aabb, minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ,
-+ maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ)
-+ && !isEmpty(minX, minY, minZ, maxX, maxY, maxZ)) {
-+ list.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ, false));
-+ ret = true;
-+ }
-+ }
-+
-+ return ret;
-+ } else {
-+ final List boxes = shape.toAabbs();
-+
-+ boolean ret = false;
-+
-+ for (int i = 0, len = boxes.size(); i < len; ++i) {
-+ final AABB box = boxes.get(i);
-+ if (voxelShapeIntersect(box, aabb) && !isEmpty(box)) {
-+ list.add(box);
-+ ret = true;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+ }
-+
-+ public static void addBoxesTo(final VoxelShape shape, final List list) {
-+ if (shape instanceof AABBVoxelShape) {
-+ final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape;
-+ if (!isEmpty(shapeCasted.aabb)) {
-+ list.add(shapeCasted.aabb);
-+ }
-+ } else if (shape instanceof ArrayVoxelShape) {
-+ final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape;
-+
-+ final double offX = shapeCasted.getOffsetX();
-+ final double offY = shapeCasted.getOffsetY();
-+ final double offZ = shapeCasted.getOffsetZ();
-+
-+ for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) {
-+ final AABB box = boundingBox.move(offX, offY, offZ);
-+ if (!isEmpty(box)) {
-+ list.add(box);
-+ }
-+ }
-+ } else {
-+ final List boxes = shape.toAabbs();
-+ for (int i = 0, len = boxes.size(); i < len; ++i) {
-+ final AABB box = boxes.get(i);
-+ if (!isEmpty(box)) {
-+ list.add(box);
-+ }
-+ }
-+ }
-+ }
-+
-+ public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) {
-+ return isAlmostCollidingOnBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
-+ }
-+
-+ public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX,
-+ final double boxMinZ, final double boxMaxZ) {
-+ final double borderMinX = worldborder.getMinX(); // -X
-+ final double borderMaxX = worldborder.getMaxX(); // +X
-+
-+ final double borderMinZ = worldborder.getMinZ(); // -Z
-+ final double borderMaxZ = worldborder.getMaxZ(); // +Z
-+
-+ return
-+ // Not intersecting if we're smaller
-+ !voxelShapeIntersect(
-+ boxMinX + COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + COLLISION_EPSILON,
-+ boxMaxX - COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - COLLISION_EPSILON,
-+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ
-+ )
-+ &&
-+
-+ // Are intersecting if we're larger
-+ voxelShapeIntersect(
-+ boxMinX - COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - COLLISION_EPSILON,
-+ boxMaxX + COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + COLLISION_EPSILON,
-+ borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ
-+ );
-+ }
-+
-+ public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final AABB boundingBox) {
-+ return isCollidingWithBorderEdge(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
-+ }
-+
-+ public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final double boxMinX, final double boxMaxX,
-+ final double boxMinZ, final double boxMaxZ) {
-+ final double borderMinX = worldborder.getMinX() + COLLISION_EPSILON; // -X
-+ final double borderMaxX = worldborder.getMaxX() - COLLISION_EPSILON; // +X
-+
-+ final double borderMinZ = worldborder.getMinZ() + COLLISION_EPSILON; // -Z
-+ final double borderMaxZ = worldborder.getMaxZ() - COLLISION_EPSILON; // +Z
-+
-+ return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ;
-+ }
-+
-+ public static boolean getCollisionsForBlocksOrWorldBorder(final CollisionGetter getter, final Entity entity, final AABB aabb,
-+ final List into, final boolean loadChunks, final boolean collidesWithUnloaded,
-+ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) {
-+ boolean ret = false;
-+
-+ if (checkBorder) {
-+ if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into);
-+ ret = true;
-+ }
-+ }
-+ }
-+
-+ int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-+ int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
-+
-+ int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1;
-+ int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1;
-+
-+ int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
-+
-+ final int minSection = WorldUtil.getMinSection(getter);
-+ final int maxSection = WorldUtil.getMaxSection(getter);
-+ final int minBlock = minSection << 4;
-+ final int maxBlock = (maxSection << 4) | 15;
-+
-+ BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+ CollisionContext collisionShape = null;
-+
-+ // special cases:
-+ if (minBlockY > maxBlock || maxBlockY < minBlock) {
-+ // no point in checking
-+ return ret;
-+ }
-+
-+ int minYIterate = Math.max(minBlock, minBlockY);
-+ int maxYIterate = Math.min(maxBlock, maxBlockY);
-+
-+ int minChunkX = minBlockX >> 4;
-+ int maxChunkX = maxBlockX >> 4;
-+
-+ int minChunkZ = minBlockZ >> 4;
-+ int maxChunkZ = maxBlockZ >> 4;
-+
-+ ServerChunkCache chunkProvider;
-+ if (getter instanceof WorldGenRegion) {
-+ chunkProvider = null;
-+ } else if (getter instanceof ServerLevel) {
-+ chunkProvider = ((ServerLevel)getter).getChunkSource();
-+ } else {
-+ chunkProvider = null;
-+ }
-+ // TODO special case single chunk?
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk
-+ int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk
-+
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk
-+ int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk
-+
-+ int chunkXGlobalPos = currChunkX << 4;
-+ int chunkZGlobalPos = currChunkZ << 4;
-+ ChunkAccess chunk;
-+ if (chunkProvider == null) {
-+ chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ);
-+ } else {
-+ chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
-+ }
-+
-+
-+ if (chunk == null) {
-+ if (collidesWithUnloaded) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ into.add(getBoxForChunk(currChunkX, currChunkZ));
-+ ret = true;
-+ }
-+ }
-+ continue;
-+ }
-+
-+ LevelChunkSection[] sections = chunk.getSections();
-+
-+ // bound y
-+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ LevelChunkSection section = sections[(currY >> 4) - minSection];
-+ if (section == null || section.isEmpty()) {
-+ // empty
-+ // skip to next section
-+ currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one
-+ continue;
-+ }
-+
-+ net.minecraft.world.level.chunk.PalettedContainer blocks = section.states;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8);
-+ int blockX = currX | chunkXGlobalPos;
-+ int blockY = currY;
-+ int blockZ = currZ | chunkZGlobalPos;
-+
-+ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
-+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
-+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0);
-+ if (edgeCount == 3) {
-+ continue;
-+ }
-+
-+ BlockState blockData = blocks.get(localBlockIndex);
-+ if (blockData.isAir()) {
-+ continue;
-+ }
-+
-+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (collisionShape == null) {
-+ collisionShape = entity == null ? CollisionContext.empty() : CollisionContext.of(entity);
-+ }
-+ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape);
-+ if (voxelshape2 != Shapes.empty()) {
-+ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ);
-+
-+ if (predicate != null && !predicate.test(blockData, mutablePos)) {
-+ continue;
-+ }
-+
-+ if (checkOnly) {
-+ if (voxelshape3.intersects(aabb)) {
-+ return true;
-+ }
-+ } else {
-+ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into);
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb,
-+ final List into, final boolean checkOnly, final Predicate predicate) {
-+ if (isEmpty(aabb) || !(getter instanceof EntityGetter entityGetter)) {
-+ return false;
-+ }
-+
-+ boolean ret = false;
-+
-+ // to comply with vanilla intersection rules, expand by -epsilon so we only get stuff we definitely collide with.
-+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
-+ // specifically with boat collisions.
-+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON);
-+ final List entities = CachedLists.getTempGetEntitiesList();
-+ try {
-+ if (entity != null && entity.hardCollides()) {
-+ entityGetter.getEntities(entity, aabb, predicate, entities);
-+ } else {
-+ entityGetter.getHardCollidingEntities(entity, aabb, predicate, entities);
-+ }
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ into.add(otherEntity.getBoundingBox());
-+ ret = true;
-+ }
-+ }
-+ }
-+ } finally {
-+ CachedLists.returnTempGetEntitiesList(entities);
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getCollisions(final CollisionGetter view, final Entity entity, final AABB aabb,
-+ final List into, final boolean loadChunks, final boolean collidesWithUnloadedChunks,
-+ final boolean checkBorder, final boolean checkOnly, final BiPredicate blockPredicate,
-+ final Predicate entityPredicate) {
-+ if (checkOnly) {
-+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate)
-+ || getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate);
-+ } else {
-+ return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate)
-+ | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate);
-+ }
-+ }
-+
-+ private CollisionUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f84060952c947d79bf2dffc61c96a300e8d7fac2
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
-@@ -0,0 +1,128 @@
-+package com.tuinity.tuinity.util;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class CoordinateUtils {
-+
-+ // dx, dz are relative to the target chunk
-+ // dx, dz in [-radius, radius]
-+ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
-+ return (dx + radius) + (2 * radius + 1)*(dz + radius);
-+ }
-+
-+ // the chunk keys are compatible with vanilla
-+
-+ public static long getChunkKey(final BlockPos pos) {
-+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final Entity entity) {
-+ return ((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));
-+ }
-+
-+ // the block coordinates are not necessarily compatible with vanilla's
-+
-+ public static int getBlockCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate);
-+ }
-+
-+ public static long getBlockKey(final int x, final int y, final int z) {
-+ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
-+ }
-+
-+ public static long getBlockKey(final BlockPos pos) {
-+ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
-+ }
-+
-+ public static long getBlockKey(final Entity entity) {
-+ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
-+ }
-+
-+ private CoordinateUtils() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
-@@ -0,0 +1,226 @@
-+package com.tuinity.tuinity.util;
-+
-+public final class IntegerUtil {
-+
-+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
-+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
-+
-+ public static int ceilLog2(final int value) {
-+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static long ceilLog2(final long value) {
-+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final int value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final long value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int roundCeilLog2(final int value) {
-+ // optimized variant of 1 << (32 - leading(val - 1))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
-+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
-+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static long roundCeilLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static int roundFloorLog2(final int value) {
-+ // optimized variant of 1 << (31 - leading(val))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
-+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
-+ }
-+
-+ public static long roundFloorLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
-+ }
-+
-+ public static boolean isPowerOfTwo(final int n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static boolean isPowerOfTwo(final long n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static int getTrailingBit(final int n) {
-+ return -n & n;
-+ }
-+
-+ public static long getTrailingBit(final long n) {
-+ return -n & n;
-+ }
-+
-+ public static int trailingZeros(final int n) {
-+ return Integer.numberOfTrailingZeros(n);
-+ }
-+
-+ public static int trailingZeros(final long n) {
-+ return Long.numberOfTrailingZeros(n);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorMultiple(final long numbers) {
-+ return (int)(numbers >>> 32);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorShift(final long numbers) {
-+ return (int)numbers;
-+ }
-+
-+ // copied from hacker's delight (signed division magic value)
-+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
-+ public static long getDivisorNumbers(final int d) {
-+ final int ad = IntegerUtil.branchlessAbs(d);
-+
-+ if (ad < 2) {
-+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
-+ }
-+
-+ final int two31 = 0x80000000;
-+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
-+
-+ int p = 31;
-+
-+ // all these variables are UNSIGNED!
-+ int t = two31 + (d >>> 31);
-+ int anc = t - 1 - t%ad;
-+ int q1 = (int)((two31 & mask)/(anc & mask));
-+ int r1 = two31 - q1*anc;
-+ int q2 = (int)((two31 & mask)/(ad & mask));
-+ int r2 = two31 - q2*ad;
-+ int delta;
-+
-+ do {
-+ p = p + 1;
-+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
-+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
-+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
-+ q1 = q1 + 1;
-+ r1 = r1 - anc;
-+ }
-+ q2 = 2*q2; // Update q2 = 2**p/|d|.
-+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
-+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
-+ q2 = q2 + 1;
-+ r2 = r2 - ad;
-+ }
-+ delta = ad - r2;
-+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
-+
-+ int magicNum = q2 + 1;
-+ if (d < 0) {
-+ magicNum = -magicNum;
-+ }
-+ int shift = p - 32;
-+ return ((long)magicNum << 32) | shift;
-+ }
-+
-+ public static int branchlessAbs(final int val) {
-+ // -n = -1 ^ n + 1
-+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ public static long branchlessAbs(final long val) {
-+ // -n = -1 ^ n + 1
-+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ //https://github.com/skeeto/hash-prospector for hash functions
-+
-+ //score = ~590.47984224483832
-+ public static int hash0(int x) {
-+ x *= 0x36935555;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ //score = ~310.01596637036749
-+ public static int hash1(int x) {
-+ x ^= x >>> 15;
-+ x *= 0x356aaaad;
-+ x ^= x >>> 17;
-+ return x;
-+ }
-+
-+ public static int hash2(int x) {
-+ x ^= x >>> 16;
-+ x *= 0x7feb352d;
-+ x ^= x >>> 15;
-+ x *= 0x846ca68b;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ public static int hash3(int x) {
-+ x ^= x >>> 17;
-+ x *= 0xed5ad4bb;
-+ x ^= x >>> 11;
-+ x *= 0xac4c1b51;
-+ x ^= x >>> 15;
-+ x *= 0x31848bab;
-+ x ^= x >>> 14;
-+ return x;
-+ }
-+
-+ //score = ~365.79959673201887
-+ public static long hash1(long x) {
-+ x ^= x >>> 27;
-+ x *= 0xb24924b71d2d354bL;
-+ x ^= x >>> 28;
-+ return x;
-+ }
-+
-+ //h2 hash
-+ public static long hash2(long x) {
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ return x;
-+ }
-+
-+ public static long hash3(long x) {
-+ x ^= x >>> 45;
-+ x *= 0xc161abe5704b6c79L;
-+ x ^= x >>> 41;
-+ x *= 0xe3e5389aedbc90f7L;
-+ x ^= x >>> 56;
-+ x *= 0x1f9aba75a52db073L;
-+ x ^= x >>> 53;
-+ return x;
-+ }
-+
-+ private IntegerUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java
-@@ -0,0 +1,100 @@
-+package com.tuinity.tuinity.util;
-+
-+public final class IntervalledCounter {
-+
-+ protected long[] times;
-+ protected final long interval;
-+ protected long minTime;
-+ protected int sum;
-+ protected int head; // inclusive
-+ protected int tail; // exclusive
-+
-+ public IntervalledCounter(final long interval) {
-+ this.times = new long[8];
-+ this.interval = interval;
-+ }
-+
-+ public void updateCurrentTime() {
-+ this.updateCurrentTime(System.nanoTime());
-+ }
-+
-+ public void updateCurrentTime(final long currentTime) {
-+ int sum = this.sum;
-+ int head = this.head;
-+ final int tail = this.tail;
-+ final long minTime = currentTime - this.interval;
-+
-+ final int arrayLen = this.times.length;
-+
-+ // guard against overflow by using subtraction
-+ while (head != tail && this.times[head] - minTime < 0) {
-+ head = (head + 1) % arrayLen;
-+ --sum;
-+ }
-+
-+ this.sum = sum;
-+ this.head = head;
-+ this.minTime = minTime;
-+ }
-+
-+ public void addTime(final long currTime) {
-+ // guard against overflow by using subtraction
-+ if (currTime - this.minTime < 0) {
-+ return;
-+ }
-+ int nextTail = (this.tail + 1) % this.times.length;
-+ if (nextTail == this.head) {
-+ this.resize();
-+ nextTail = (this.tail + 1) % this.times.length;
-+ }
-+
-+ this.times[this.tail] = currTime;
-+ this.tail = nextTail;
-+ }
-+
-+ public void updateAndAdd(final int count) {
-+ final long currTime = System.nanoTime();
-+ this.updateCurrentTime(currTime);
-+ for (int i = 0; i < count; ++i) {
-+ this.addTime(currTime);
-+ }
-+ }
-+
-+ public void updateAndAdd(final int count, final long currTime) {
-+ this.updateCurrentTime(currTime);
-+ for (int i = 0; i < count; ++i) {
-+ this.addTime(currTime);
-+ }
-+ }
-+
-+ private void resize() {
-+ final long[] oldElements = this.times;
-+ final long[] newElements = new long[this.times.length * 2];
-+ this.times = newElements;
-+
-+ final int head = this.head;
-+ final int tail = this.tail;
-+ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
-+ this.head = 0;
-+ this.tail = size;
-+
-+ if (tail >= head) {
-+ System.arraycopy(oldElements, head, newElements, 0, size);
-+ } else {
-+ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
-+ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
-+ }
-+ }
-+
-+ // returns in units per second
-+ public double getRate() {
-+ return this.size() / (this.interval * 1.0e-9);
-+ }
-+
-+ public int size() {
-+ final int head = this.head;
-+ final int tail = this.tail;
-+
-+ return tail >= head ? (tail - head) : (tail + (this.times.length - head));
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/PoiAccess.java b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e99583529a2cbdf8b764be3dff4373ec0ffaecd7
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/PoiAccess.java
-@@ -0,0 +1,748 @@
-+package com.tuinity.tuinity.util;
-+
-+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
-+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.ai.village.poi.PoiManager;
-+import net.minecraft.world.entity.ai.village.poi.PoiRecord;
-+import net.minecraft.world.entity.ai.village.poi.PoiSection;
-+import net.minecraft.world.entity.ai.village.poi.PoiType;
-+import java.util.ArrayList;
-+import java.util.HashSet;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.Optional;
-+import java.util.Set;
-+import java.util.function.Predicate;
-+
-+/**
-+ * Provides optimised access to POI data. All returned values will be identical to vanilla.
-+ */
-+public final class PoiAccess {
-+
-+ protected static double clamp(final double val, final double min, final double max) {
-+ return (val < min ? min : (val > max ? max : val));
-+ }
-+
-+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ,
-+ final double boxMaxX, final double boxMaxY, final double boxMaxZ,
-+
-+ final double circleX, final double circleY, final double circleZ) {
-+ // is the circle center inside the box?
-+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) {
-+ return 0.0;
-+ }
-+
-+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0;
-+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0;
-+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0;
-+
-+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0;
-+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0;
-+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0;
-+
-+ double centerDiffX = circleX - boxCenterX;
-+ double centerDiffY = circleY - boxCenterY;
-+ double centerDiffZ = circleZ - boxCenterZ;
-+
-+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX);
-+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY);
-+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ);
-+
-+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ);
-+ }
-+
-+
-+ // key is:
-+ // upper 32 bits:
-+ // upper 16 bits: max y section
-+ // lower 16 bits: min y section
-+ // lower 32 bits:
-+ // upper 16 bits: section
-+ // lower 16 bits: radius
-+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) {
-+ return (
-+ (maxSection & 0xFFFFL) << (64 - 16)
-+ | (minSection & 0xFFFFL) << (64 - 32)
-+ | (section & 0xFFFFL) << (64 - 48)
-+ | (radius & 0xFFFFL) << (64 - 64)
-+ );
-+ }
-+
-+ // only includes x/z axis
-+ // finds the closest poi data by distance.
-+ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final PoiRecord ret = findClosestPoiDataRecord(
-+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
-+ );
-+
-+ return ret == null ? null : ret.getPos();
-+ }
-+
-+ // only includes x/z axis
-+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
-+ public static void findClosestPoiDataPositions(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final Set ret) {
-+ final Set positions = new HashSet<>();
-+ // pos predicate is last thing that runs before adding to ret.
-+ final Predicate newPredicate = (final BlockPos pos) -> {
-+ if (positionPredicate != null && !positionPredicate.test(pos)) {
-+ return false;
-+ }
-+ return positions.add(pos.immutable());
-+ };
-+
-+ final List toConvert = new ArrayList<>();
-+ findClosestPoiDataRecords(
-+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, toConvert
-+ );
-+
-+ for (final PoiRecord record : toConvert) {
-+ ret.add(record.getPos());
-+ }
-+ }
-+
-+ // only includes x/z axis
-+ // finds the closest poi data by distance.
-+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final List ret = new ArrayList<>();
-+ findClosestPoiDataRecords(
-+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load, ret
-+ );
-+ return ret.isEmpty() ? null : ret.get(0);
-+ }
-+
-+ // only includes x/z axis
-+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
-+ public static void findClosestPoiDataRecords(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final List ret) {
-+ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
-+
-+ final List closestRecords = new ArrayList<>();
-+ double closestDistanceSquared = maxDistance * maxDistance;
-+
-+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
-+ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
-+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
-+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
-+ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
-+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
-+
-+ final int centerX = sourcePosition.getX() >> 4;
-+ final int centerY = sourcePosition.getY() >> 4;
-+ final int centerZ = sourcePosition.getZ() >> 4;
-+
-+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
-+ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
-+ final LongOpenHashSet seen = new LongOpenHashSet();
-+
-+ while (!queue.isEmpty()) {
-+ final long key = queue.dequeueLong();
-+ final int sectionX = CoordinateUtils.getChunkSectionX(key);
-+ final int sectionY = CoordinateUtils.getChunkSectionY(key);
-+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
-+
-+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
-+ // out of bound chunk
-+ continue;
-+ }
-+
-+ final double sectionDistanceSquared = getSmallestDistanceSquared(
-+ (sectionX << 4) + 0.5,
-+ (sectionY << 4) + 0.5,
-+ (sectionZ << 4) + 0.5,
-+ (sectionX << 4) + 15.5,
-+ (sectionY << 4) + 15.5,
-+ (sectionZ << 4) + 15.5,
-+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ()
-+ );
-+ if (sectionDistanceSquared > closestDistanceSquared) {
-+ continue;
-+ }
-+
-+ // queue all neighbours
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ for (int dy = -1; dy <= 1; ++dy) {
-+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
-+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
-+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
-+ continue;
-+ }
-+
-+ final int neighbourX = sectionX + dx;
-+ final int neighbourY = sectionY + dy;
-+ final int neighbourZ = sectionZ + dz;
-+
-+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
-+ if (seen.add(neighbourKey)) {
-+ queue.enqueue(neighbourKey);
-+ }
-+ }
-+ }
-+ }
-+
-+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
-+
-+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
-+ continue;
-+ }
-+
-+ final PoiSection poiSection = poiSectionOptional.orElse(null);
-+
-+ final Map> sectionData = poiSection.getData();
-+ if (sectionData.isEmpty()) {
-+ continue;
-+ }
-+
-+ // now we search the section data
-+ for (final Map.Entry> entry : sectionData.entrySet()) {
-+ if (!villagePlaceType.test(entry.getKey())) {
-+ // filter out by poi type
-+ continue;
-+ }
-+
-+ // now we can look at the poi data
-+ for (final PoiRecord poiData : entry.getValue()) {
-+ if (!occupancyFilter.test(poiData)) {
-+ // filter by occupancy
-+ continue;
-+ }
-+
-+ final BlockPos poiPosition = poiData.getPos();
-+
-+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
-+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
-+ // out of range for square radius
-+ continue;
-+ }
-+
-+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
-+ final double dataRange = poiPosition.distSqr(sourcePosition);
-+
-+ if (dataRange > closestDistanceSquared) {
-+ // out of range for distance check
-+ continue;
-+ }
-+
-+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
-+ // filter by position
-+ continue;
-+ }
-+
-+ if (dataRange < closestDistanceSquared) {
-+ closestRecords.clear();
-+ closestDistanceSquared = dataRange;
-+ }
-+ closestRecords.add(poiData);
-+ }
-+ }
-+ }
-+
-+ // uh oh! we might have multiple records that match the distance sorting!
-+ // we need to re-order our results by the way vanilla would have iterated over them.
-+ closestRecords.sort((record1, record2) -> {
-+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
-+ // is fine and should be preserved (this sort is stable so we're good there)
-+ // but they iterate sections by x then by z (like the following)
-+ // for (int x = -dx; x <= dx; ++x)
-+ // for (int z = -dz; z <= dz; ++z)
-+ // ....
-+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
-+ final BlockPos pos1 = record1.getPos();
-+ final BlockPos pos2 = record2.getPos();
-+
-+ final int cx1 = pos1.getX() >> 4;
-+ final int cz1 = pos1.getZ() >> 4;
-+
-+ final int cx2 = pos2.getX() >> 4;
-+ final int cz2 = pos2.getZ() >> 4;
-+
-+ if (cz2 != cz1) {
-+ // want smaller z
-+ return Integer.compare(cz1, cz2);
-+ }
-+
-+ if (cx2 != cx1) {
-+ // want smaller x
-+ return Integer.compare(cx1, cx2);
-+ }
-+
-+ // same chunk
-+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
-+ // so now we just compare section y, wanting smaller y
-+
-+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
-+ });
-+
-+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
-+ ret.addAll(closestRecords);
-+ }
-+
-+ // finds the closest poi entry pos.
-+ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final PoiRecord ret = findNearestPoiRecord(
-+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
-+ );
-+ return ret == null ? null : ret.getPos();
-+ }
-+
-+ // finds the closest `max` poi entry positions.
-+ public static void findNearestPoiPositions(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final int max,
-+ final List ret) {
-+ final Set positions = new HashSet<>();
-+ // pos predicate is last thing that runs before adding to ret.
-+ final Predicate newPredicate = (final BlockPos pos) -> {
-+ if (positionPredicate != null && !positionPredicate.test(pos)) {
-+ return false;
-+ }
-+ return positions.add(pos.immutable());
-+ };
-+
-+ final List toConvert = new ArrayList<>();
-+ findNearestPoiRecords(
-+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistance, occupancy, load, max, toConvert
-+ );
-+
-+ for (final PoiRecord record : toConvert) {
-+ ret.add(record.getPos());
-+ }
-+ }
-+
-+ // finds the closest poi entry.
-+ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final List ret = new ArrayList<>();
-+ findNearestPoiRecords(
-+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load,
-+ 1, ret
-+ );
-+ return ret.isEmpty() ? null : ret.get(0);
-+ }
-+
-+ // finds the closest `max` poi entries.
-+ public static void findNearestPoiRecords(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ // position predicate must not modify chunk POI
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final double maxDistance,
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final int max,
-+ final List ret) {
-+ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
-+
-+ final double maxDistanceSquared = maxDistance * maxDistance;
-+ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>();
-+ int totalRecords = 0;
-+ double furthestDistanceSquared = maxDistanceSquared;
-+
-+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
-+ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
-+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
-+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
-+ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
-+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
-+
-+ final int centerX = sourcePosition.getX() >> 4;
-+ final int centerY = sourcePosition.getY() >> 4;
-+ final int centerZ = sourcePosition.getZ() >> 4;
-+
-+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
-+ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
-+ final LongOpenHashSet seen = new LongOpenHashSet();
-+
-+ while (!queue.isEmpty()) {
-+ final long key = queue.dequeueLong();
-+ final int sectionX = CoordinateUtils.getChunkSectionX(key);
-+ final int sectionY = CoordinateUtils.getChunkSectionY(key);
-+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
-+
-+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
-+ // out of bound chunk
-+ continue;
-+ }
-+
-+ final double sectionDistanceSquared = getSmallestDistanceSquared(
-+ (sectionX << 4) + 0.5,
-+ (sectionY << 4) + 0.5,
-+ (sectionZ << 4) + 0.5,
-+ (sectionX << 4) + 15.5,
-+ (sectionY << 4) + 15.5,
-+ (sectionZ << 4) + 15.5,
-+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ()
-+ );
-+
-+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
-+ continue;
-+ }
-+
-+ // queue all neighbours
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ for (int dy = -1; dy <= 1; ++dy) {
-+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
-+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
-+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
-+ continue;
-+ }
-+
-+ final int neighbourX = sectionX + dx;
-+ final int neighbourY = sectionY + dy;
-+ final int neighbourZ = sectionZ + dz;
-+
-+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
-+ if (seen.add(neighbourKey)) {
-+ queue.enqueue(neighbourKey);
-+ }
-+ }
-+ }
-+ }
-+
-+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
-+
-+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
-+ continue;
-+ }
-+
-+ final PoiSection poiSection = poiSectionOptional.orElse(null);
-+
-+ final Map> sectionData = poiSection.getData();
-+ if (sectionData.isEmpty()) {
-+ continue;
-+ }
-+
-+ // now we search the section data
-+ for (final Map.Entry> entry : sectionData.entrySet()) {
-+ if (!villagePlaceType.test(entry.getKey())) {
-+ // filter out by poi type
-+ continue;
-+ }
-+
-+ // now we can look at the poi data
-+ for (final PoiRecord poiData : entry.getValue()) {
-+ if (!occupancyFilter.test(poiData)) {
-+ // filter by occupancy
-+ continue;
-+ }
-+
-+ final BlockPos poiPosition = poiData.getPos();
-+
-+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
-+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
-+ // out of range for square radius
-+ continue;
-+ }
-+
-+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
-+ final double dataRange = poiPosition.distSqr(sourcePosition);
-+
-+ if (dataRange > maxDistanceSquared) {
-+ // out of range for distance check
-+ continue;
-+ }
-+
-+ if (dataRange > furthestDistanceSquared && totalRecords >= max) {
-+ // out of range for distance check
-+ continue;
-+ }
-+
-+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
-+ // filter by position
-+ continue;
-+ }
-+
-+ if (dataRange > furthestDistanceSquared) {
-+ // we know totalRecords < max, so this entry is now our furthest
-+ furthestDistanceSquared = dataRange;
-+ }
-+
-+ closestRecords.computeIfAbsent(dataRange, (final double unused) -> {
-+ return new ArrayList<>();
-+ }).add(poiData);
-+
-+ if (++totalRecords >= max) {
-+ if (closestRecords.size() >= 2) {
-+ int entriesInClosest = 0;
-+ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator();
-+ double nextFurthestDistanceSquared = 0.0;
-+
-+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) {
-+ final Double2ObjectMap.Entry> recordEntry = iterator.next();
-+ entriesInClosest += recordEntry.getValue().size();
-+ nextFurthestDistanceSquared = recordEntry.getDoubleKey();
-+ }
-+
-+ if (entriesInClosest >= max) {
-+ // the last set of entries at range wont even be considered for sure... nuke em
-+ final Double2ObjectMap.Entry> recordEntry = iterator.next();
-+ totalRecords -= recordEntry.getValue().size();
-+ iterator.remove();
-+
-+ furthestDistanceSquared = nextFurthestDistanceSquared;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ final List closestRecordsUnsorted = new ArrayList<>();
-+
-+ // we're done here, so now just flatten the map and sort it.
-+
-+ for (final List records : closestRecords.values()) {
-+ closestRecordsUnsorted.addAll(records);
-+ }
-+
-+ // uh oh! we might have multiple records that match the distance sorting!
-+ // we need to re-order our results by the way vanilla would have iterated over them.
-+ closestRecordsUnsorted.sort((record1, record2) -> {
-+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
-+ // is fine and should be preserved (this sort is stable so we're good there)
-+ // but they iterate sections by x then by z (like the following)
-+ // for (int x = -dx; x <= dx; ++x)
-+ // for (int z = -dz; z <= dz; ++z)
-+ // ....
-+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
-+ final BlockPos pos1 = record1.getPos();
-+ final BlockPos pos2 = record2.getPos();
-+
-+ final int cx1 = pos1.getX() >> 4;
-+ final int cz1 = pos1.getZ() >> 4;
-+
-+ final int cx2 = pos2.getX() >> 4;
-+ final int cz2 = pos2.getZ() >> 4;
-+
-+ if (cz2 != cz1) {
-+ // want smaller z
-+ return Integer.compare(cz1, cz2);
-+ }
-+
-+ if (cx2 != cx1) {
-+ // want smaller x
-+ return Integer.compare(cx1, cx2);
-+ }
-+
-+ // same chunk
-+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
-+ // so now we just compare section y, wanting smaller section y
-+
-+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
-+ });
-+
-+ // trim out any entries exceeding our maximum
-+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) {
-+ closestRecordsUnsorted.remove(i);
-+ }
-+
-+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
-+ ret.addAll(closestRecordsUnsorted);
-+ }
-+
-+ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final PoiRecord ret = findAnyPoiRecord(
-+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load
-+ );
-+
-+ return ret == null ? null : ret.getPos();
-+ }
-+
-+ public static void findAnyPoiPositions(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final int max,
-+ final List ret) {
-+ final Set positions = new HashSet<>();
-+ // pos predicate is last thing that runs before adding to ret.
-+ final Predicate newPredicate = (final BlockPos pos) -> {
-+ if (positionPredicate != null && !positionPredicate.test(pos)) {
-+ return false;
-+ }
-+ return positions.add(pos.immutable());
-+ };
-+
-+ final List toConvert = new ArrayList<>();
-+ findAnyPoiRecords(
-+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert
-+ );
-+
-+ for (final PoiRecord record : toConvert) {
-+ ret.add(record.getPos());
-+ }
-+ }
-+
-+ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load) {
-+ final List ret = new ArrayList<>();
-+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret);
-+ return ret.isEmpty() ? null : ret.get(0);
-+ }
-+
-+ public static void findAnyPoiRecords(final PoiManager poiStorage,
-+ final Predicate villagePlaceType,
-+ final Predicate positionPredicate,
-+ final BlockPos sourcePosition,
-+ final int range, // distance on x y z axis
-+ final PoiManager.Occupancy occupancy,
-+ final boolean load,
-+ final int max,
-+ final List ret) {
-+ // the biggest issue with the original mojang implementation is that they chain so many streams together
-+ // the amount of streams chained just rolls performance, even if nothing is iterated over
-+ final Predicate super PoiRecord> occupancyFilter = occupancy.getTest();
-+ final double rangeSquared = range * range;
-+
-+ int added = 0;
-+
-+ // First up, we need to iterate the chunks
-+ // all the values here are in chunk sections
-+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
-+ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4);
-+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
-+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
-+ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4);
-+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
-+
-+ // Vanilla iterates by x until max is reached then increases z
-+ // vanilla also searches by increasing Y section value
-+ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) {
-+ for (int currX = lowerX; currX <= upperX; ++currX) {
-+ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need
-+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) :
-+ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ));
-+ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null);
-+ if (poiSection == null) {
-+ continue;
-+ }
-+
-+ final Map> sectionData = poiSection.getData();
-+ if (sectionData.isEmpty()) {
-+ continue;
-+ }
-+
-+ // now we search the section data
-+ for (final Map.Entry> entry : sectionData.entrySet()) {
-+ if (!villagePlaceType.test(entry.getKey())) {
-+ // filter out by poi type
-+ continue;
-+ }
-+
-+ // now we can look at the poi data
-+ for (final PoiRecord poiData : entry.getValue()) {
-+ if (!occupancyFilter.test(poiData)) {
-+ // filter by occupancy
-+ continue;
-+ }
-+
-+ final BlockPos poiPosition = poiData.getPos();
-+
-+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
-+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
-+ // out of range for square radius
-+ continue;
-+ }
-+
-+ if (poiPosition.distSqr(sourcePosition) > rangeSquared) {
-+ // out of range for distance check
-+ continue;
-+ }
-+
-+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
-+ // filter by position
-+ continue;
-+ }
-+
-+ // found one!
-+ ret.add(poiData);
-+ if (++added >= max) {
-+ return;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ private PoiAccess() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a08377e4b0d9c2d78cf851e2c72770cf623de51a
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/TickThread.java
-@@ -0,0 +1,41 @@
-+package com.tuinity.tuinity.util;
-+
-+import net.minecraft.server.MinecraftServer;
-+import org.bukkit.Bukkit;
-+
-+public final class TickThread extends Thread {
-+
-+ public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("tuinity.strict-thread-checks");
-+
-+ static {
-+ if (STRICT_THREAD_CHECKS) {
-+ MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer");
-+ }
-+ }
-+
-+ public static void softEnsureTickThread(final String reason) {
-+ if (!STRICT_THREAD_CHECKS) {
-+ return;
-+ }
-+ ensureTickThread(reason);
-+ }
-+
-+
-+ public static void ensureTickThread(final String reason) {
-+ if (!Bukkit.isPrimaryThread()) {
-+ MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+ throw new IllegalStateException(reason);
-+ }
-+ }
-+
-+ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
-+
-+ public TickThread(final Runnable run, final String name, final int id) {
-+ super(run, name);
-+ this.id = id;
-+ }
-+
-+ public static TickThread getCurrentTickThread() {
-+ return (TickThread)Thread.currentThread();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..958b3aff3dda64323456d7e0ef0346a72d43f3f1
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
-@@ -0,0 +1,46 @@
-+package com.tuinity.tuinity.util;
-+
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+ // min, max are inclusive
-+
-+ public static int getMaxSection(final LevelHeightAccessor world) {
-+ return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+ }
-+
-+ public static int getMinSection(final LevelHeightAccessor world) {
-+ return world.getMinSection();
-+ }
-+
-+ public static int getMaxLightSection(final LevelHeightAccessor world) {
-+ return getMaxSection(world) + 1;
-+ }
-+
-+ public static int getMinLightSection(final LevelHeightAccessor world) {
-+ return getMinSection(world) - 1;
-+ }
-+
-+
-+
-+ public static int getTotalSections(final LevelHeightAccessor world) {
-+ return getMaxSection(world) - getMinSection(world) + 1;
-+ }
-+
-+ public static int getTotalLightSections(final LevelHeightAccessor world) {
-+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+ }
-+
-+ public static int getMinBlockY(final LevelHeightAccessor world) {
-+ return getMinSection(world) << 4;
-+ }
-+
-+ public static int getMaxBlockY(final LevelHeightAccessor world) {
-+ return (getMaxSection(world) << 4) | 15;
-+ }
-+
-+ private WorldUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..98a1be343d81d6431476fea6a68014def8ce923b
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
-@@ -0,0 +1,334 @@
-+package com.tuinity.tuinity.util.maplist;
-+
-+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-+import org.bukkit.Bukkit;
-+import java.util.Arrays;
-+import java.util.NoSuchElementException;
-+
-+public final class IteratorSafeOrderedReferenceSet {
-+
-+ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
-+
-+ protected final Reference2IntLinkedOpenHashMap indexMap;
-+ protected int firstInvalidIndex = -1;
-+
-+ /* list impl */
-+ protected E[] listElements;
-+ protected int listSize;
-+
-+ protected final double maxFragFactor;
-+
-+ protected int iteratorCount;
-+
-+ private final boolean threadRestricted;
-+
-+ public IteratorSafeOrderedReferenceSet() {
-+ this(16, 0.75f, 16, 0.2);
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) {
-+ this(16, 0.75f, 16, 0.2, threadRestricted);
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
-+ final double maxFragFactor) {
-+ this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false);
-+ }
-+ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
-+ final double maxFragFactor, final boolean threadRestricted) {
-+ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
-+ this.indexMap.defaultReturnValue(-1);
-+ this.maxFragFactor = maxFragFactor;
-+ this.listElements = (E[])new Object[arrayCapacity];
-+ this.threadRestricted = threadRestricted;
-+ }
-+
-+ /*
-+ 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());
-+ }
-+ }
-+ */
-+
-+ protected final boolean allowSafeIteration() {
-+ return !this.threadRestricted || Bukkit.isPrimaryThread();
-+ }
-+
-+ protected final double getFragFactor() {
-+ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
-+ }
-+
-+ public int createRawIterator() {
-+ if (this.allowSafeIteration()) {
-+ ++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.allowSafeIteration() && --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.allowSafeIteration() && 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;
-+ }
-+
-+ protected 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) {
-+ if (this.allowSafeIteration()) {
-+ ++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();
-+
-+ }
-+
-+ protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator {
-+
-+ protected final IteratorSafeOrderedReferenceSet set;
-+ protected final boolean canFinish;
-+ protected final int maxIndex;
-+ protected int nextIndex;
-+ protected E pendingValue;
-+ protected boolean finished;
-+ protected E lastReturned;
-+
-+ protected 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;
-+ if (this.set.allowSafeIteration()) {
-+ this.set.finishRawIterator();
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7f2aeb5ed6775ddf38f2561aae8a82f99c8413a7
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/math/ThreadUnsafeRandom.java
-@@ -0,0 +1,46 @@
-+package com.tuinity.tuinity.util.math;
-+
-+import java.util.Random;
-+
-+public final class ThreadUnsafeRandom extends Random {
-+
-+ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them.
-+ private static final long multiplier = 0x5DEECE66DL;
-+ private static final long addend = 0xBL;
-+ private static final long mask = (1L << 48) - 1;
-+
-+ private static long initialScramble(long seed) {
-+ return (seed ^ multiplier) & mask;
-+ }
-+
-+ private long seed;
-+
-+ @Override
-+ public void setSeed(long seed) {
-+ // note: called by Random constructor
-+ this.seed = initialScramble(seed);
-+ }
-+
-+ @Override
-+ protected int next(int bits) {
-+ // avoid the expensive CAS logic used by superclass
-+ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits));
-+ }
-+
-+ // Taken from
-+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
-+ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c
-+ // Original license is public domain
-+ public static int fastRandomBounded(final long randomInteger, final long limit) {
-+ // randomInteger must be [0, pow(2, 32))
-+ // limit must be [0, pow(2, 32))
-+ return (int)((randomInteger * limit) >>> 32);
-+ }
-+
-+ @Override
-+ public int nextInt(int bound) {
-+ // yes this breaks random's spec
-+ // however there's nothing that uses this class that relies on it
-+ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound);
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..b8fa9cd9bce312fc85b90e17094241216c620a9e
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java
-@@ -0,0 +1,297 @@
-+package com.tuinity.tuinity.util.misc;
-+
-+import com.tuinity.tuinity.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/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..cdd3c4032c1d6b34a10ba415bd4d0e377aa9af3c
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java
-@@ -0,0 +1,718 @@
-+package com.tuinity.tuinity.util.misc;
-+
-+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;
-+import net.minecraft.server.MCUtil;
-+
-+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(MCUtil.getCoordinateKey(x, z));
-+ }
-+
-+ public void setSource(final int x, final int z, final int level) {
-+ this.setSource(MCUtil.getCoordinateKey(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(MCUtil.getCoordinateKey(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 = MCUtil.getCoordinateKey(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 = MCUtil.getCoordinateKey(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/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3bd1bfab37c8a3b981c86ff09941590f028d24bc
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/util/table/ZeroCollidingReferenceStateTable.java
-@@ -0,0 +1,160 @@
-+package com.tuinity.tuinity.util.table;
-+
-+import com.google.common.collect.Table;
-+import net.minecraft.world.level.block.state.StateHolder;
-+import net.minecraft.world.level.block.state.properties.Property;
-+import java.util.Collection;
-+import java.util.HashSet;
-+import java.util.Map;
-+import java.util.Set;
-+
-+public final class ZeroCollidingReferenceStateTable {
-+
-+ // upper 32 bits: starting index
-+ // lower 32 bits: bitset for contained ids
-+ protected final long[] this_index_table;
-+ protected final Comparable>[] this_table;
-+ protected final StateHolder, ?> this_state;
-+
-+ protected long[] index_table;
-+ protected StateHolder, ?>[][] value_table;
-+
-+ public ZeroCollidingReferenceStateTable(final StateHolder, ?> state, final Map, Comparable>> this_map) {
-+ this.this_state = state;
-+ this.this_index_table = this.create_table(this_map.keySet());
-+
-+ int max_id = -1;
-+ for (final Property> property : this_map.keySet()) {
-+ final int id = lookup_vindex(property, this.this_index_table);
-+ if (id > max_id) {
-+ max_id = id;
-+ }
-+ }
-+
-+ this.this_table = new Comparable[max_id + 1];
-+ for (final Map.Entry, Comparable>> entry : this_map.entrySet()) {
-+ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue();
-+ }
-+ }
-+
-+ public void loadInTable(final Table, Comparable>, StateHolder, ?>> table,
-+ final Map, Comparable>> this_map) {
-+ final Set> combined = new HashSet<>(table.rowKeySet());
-+ combined.addAll(this_map.keySet());
-+
-+ this.index_table = this.create_table(combined);
-+
-+ int max_id = -1;
-+ for (final Property> property : combined) {
-+ final int id = lookup_vindex(property, this.index_table);
-+ if (id > max_id) {
-+ max_id = id;
-+ }
-+ }
-+
-+ this.value_table = new StateHolder[max_id + 1][];
-+
-+ final Map, Map, StateHolder, ?>>> map = table.rowMap();
-+ for (final Property> property : map.keySet()) {
-+ final Map, StateHolder, ?>> propertyMap = map.get(property);
-+
-+ final int id = lookup_vindex(property, this.index_table);
-+ final StateHolder, ?>[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()];
-+
-+ for (final Map.Entry, StateHolder, ?>> entry : propertyMap.entrySet()) {
-+ if (entry.getValue() == null) {
-+ // TODO what
-+ continue;
-+ }
-+
-+ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue();
-+ }
-+ }
-+
-+
-+ for (final Map.Entry, Comparable>> entry : this_map.entrySet()) {
-+ final Property> property = entry.getKey();
-+ final int index = lookup_vindex(property, this.index_table);
-+
-+ if (this.value_table[index] == null) {
-+ this.value_table[index] = new StateHolder[property.getPossibleValues().size()];
-+ }
-+
-+ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state;
-+ }
-+ }
-+
-+
-+ protected long[] create_table(final Collection> collection) {
-+ int max_id = -1;
-+ for (final Property> property : collection) {
-+ final int id = property.getId();
-+ if (id > max_id) {
-+ max_id = id;
-+ }
-+ }
-+
-+ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32)
-+
-+ for (final Property> property : collection) {
-+ final int id = property.getId();
-+
-+ ret[id >>> 5] |= (1L << (id & 31));
-+ }
-+
-+ int total = 0;
-+ for (int i = 1, len = ret.length; i < len; ++i) {
-+ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32;
-+ }
-+
-+ return ret;
-+ }
-+
-+ public Comparable> get(final Property> state) {
-+ final Comparable>[] table = this.this_table;
-+ final int index = lookup_vindex(state, this.this_index_table);
-+
-+ if (index < 0 || index >= table.length) {
-+ return null;
-+ }
-+ return table[index];
-+ }
-+
-+ public StateHolder, ?> get(final Property> property, final Comparable> with) {
-+ final int withId = ((Property)property).getIdFor(with);
-+ if (withId < 0) {
-+ return null;
-+ }
-+
-+ final int index = lookup_vindex(property, this.index_table);
-+ final StateHolder, ?>[][] table = this.value_table;
-+ if (index < 0 || index >= table.length) {
-+ return null;
-+ }
-+
-+ final StateHolder, ?>[] values = table[index];
-+
-+ if (withId >= values.length) {
-+ return null;
-+ }
-+
-+ return values[withId];
-+ }
-+
-+ protected static int lookup_vindex(final Property> property, final long[] index_table) {
-+ final int id = property.getId();
-+ final long bitset_mask = (1L << (id & 31));
-+ final long lower_mask = bitset_mask - 1;
-+ final int index = id >>> 5;
-+ if (index >= index_table.length) {
-+ return -1;
-+ }
-+ final long index_value = index_table[index];
-+ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain
-+
-+ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id
-+ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0,
-+ // otherwise it comes out as -1.
-+ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check);
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..370c070dedd169fe85ad2cb488dae7aa1dcf28fc
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java
-@@ -0,0 +1,200 @@
-+package com.tuinity.tuinity.voxel;
-+
-+import com.tuinity.tuinity.util.CollisionUtil;
-+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
-+import it.unimi.dsi.fastutil.doubles.DoubleList;
-+import net.minecraft.core.Direction;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.List;
-+
-+public final class AABBVoxelShape extends VoxelShape {
-+
-+ public final AABB aabb;
-+
-+ public AABBVoxelShape(AABB aabb) {
-+ super(Shapes.getFullUnoptimisedCube().shape);
-+ this.aabb = aabb;
-+ }
-+
-+ @Override
-+ public boolean isEmpty() {
-+ return CollisionUtil.isEmpty(this.aabb);
-+ }
-+
-+ @Override
-+ public double min(Direction.Axis enumdirection_enumaxis) {
-+ switch (enumdirection_enumaxis.ordinal()) {
-+ case 0:
-+ return this.aabb.minX;
-+ case 1:
-+ return this.aabb.minY;
-+ case 2:
-+ return this.aabb.minZ;
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ @Override
-+ public double max(Direction.Axis enumdirection_enumaxis) {
-+ switch (enumdirection_enumaxis.ordinal()) {
-+ case 0:
-+ return this.aabb.maxX;
-+ case 1:
-+ return this.aabb.maxY;
-+ case 2:
-+ return this.aabb.maxZ;
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ @Override
-+ public AABB bounds() {
-+ return this.aabb;
-+ }
-+
-+ // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis.
-+ @Override
-+ protected double get(Direction.Axis enumdirection_enumaxis, int i) {
-+ switch (enumdirection_enumaxis.ordinal() | (i << 2)) {
-+ case (0 | (0 << 2)):
-+ return this.aabb.minX;
-+ case (1 | (0 << 2)):
-+ return this.aabb.minY;
-+ case (2 | (0 << 2)):
-+ return this.aabb.minZ;
-+ case (0 | (1 << 2)):
-+ return this.aabb.maxX;
-+ case (1 | (1 << 2)):
-+ return this.aabb.maxY;
-+ case (2 | (1 << 2)):
-+ return this.aabb.maxZ;
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ private DoubleList cachedListX;
-+ private DoubleList cachedListY;
-+ private DoubleList cachedListZ;
-+
-+ @Override
-+ protected DoubleList getCoords(Direction.Axis enumdirection_enumaxis) {
-+ switch (enumdirection_enumaxis.ordinal()) {
-+ case 0:
-+ return this.cachedListX == null ? this.cachedListX = DoubleArrayList.wrap(new double[] { this.aabb.minX, this.aabb.maxX }) : this.cachedListX;
-+ case 1:
-+ return this.cachedListY == null ? this.cachedListY = DoubleArrayList.wrap(new double[] { this.aabb.minY, this.aabb.maxY }) : this.cachedListY;
-+ case 2:
-+ return this.cachedListZ == null ? this.cachedListZ = DoubleArrayList.wrap(new double[] { this.aabb.minZ, this.aabb.maxZ }) : this.cachedListZ;
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ @Override
-+ public VoxelShape move(double d0, double d1, double d2) {
-+ return new AABBVoxelShape(this.aabb.move(d0, d1, d2));
-+ }
-+
-+ @Override
-+ public VoxelShape optimize() {
-+ if (this.isEmpty()) {
-+ return Shapes.empty();
-+ } else if (this == Shapes.BLOCK_OPTIMISED || this.aabb.equals(Shapes.BLOCK_OPTIMISED.aabb)) {
-+ return Shapes.BLOCK_OPTIMISED;
-+ }
-+ return this;
-+ }
-+
-+ @Override
-+ public void forAllBoxes(Shapes.DoubleLineConsumer voxelshapes_a) {
-+ voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ);
-+ }
-+
-+ @Override
-+ public List toAabbs() { // getAABBs
-+ List ret = new ArrayList<>(1);
-+ ret.add(this.aabb);
-+ return ret;
-+ }
-+
-+ @Override
-+ protected int findIndex(Direction.Axis enumdirection_enumaxis, double d0) { // findPointIndexAfterOffset
-+ switch (enumdirection_enumaxis.ordinal()) {
-+ case 0:
-+ return d0 < this.aabb.maxX ? (d0 < this.aabb.minX ? -1 : 0) : 1;
-+ case 1:
-+ return d0 < this.aabb.maxY ? (d0 < this.aabb.minY ? -1 : 0) : 1;
-+ case 2:
-+ return d0 < this.aabb.maxZ ? (d0 < this.aabb.minZ ? -1 : 0) : 1;
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ @Override
-+ protected VoxelShape calculateFace(Direction direction) {
-+ if (this.isEmpty()) {
-+ return Shapes.empty();
-+ }
-+ if (this == Shapes.BLOCK_OPTIMISED) {
-+ return this;
-+ }
-+ switch (direction) {
-+ case EAST: // +X
-+ case WEST: { // -X
-+ final double from = direction == Direction.EAST ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON;
-+ if (from > this.aabb.maxX || this.aabb.minX > from) {
-+ return Shapes.empty();
-+ }
-+ return new AABBVoxelShape(new AABB(0.0, this.aabb.minY, this.aabb.minZ, 1.0, this.aabb.maxY, this.aabb.maxZ)).optimize();
-+ }
-+ case UP: // +Y
-+ case DOWN: { // -Y
-+ final double from = direction == Direction.UP ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON;
-+ if (from > this.aabb.maxY || this.aabb.minY > from) {
-+ return Shapes.empty();
-+ }
-+ return new AABBVoxelShape(new AABB(this.aabb.minX, 0.0, this.aabb.minZ, this.aabb.maxX, 1.0, this.aabb.maxZ)).optimize();
-+ }
-+ case SOUTH: // +Z
-+ case NORTH: { // -Z
-+ final double from = direction == Direction.SOUTH ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON;
-+ if (from > this.aabb.maxZ || this.aabb.minZ > from) {
-+ return Shapes.empty();
-+ }
-+ return new AABBVoxelShape(new AABB(this.aabb.minX, this.aabb.minY, 0.0, this.aabb.maxX, this.aabb.maxY, 1.0)).optimize();
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public double collide(Direction.Axis enumdirection_enumaxis, AABB axisalignedbb, double d0) {
-+ if (CollisionUtil.isEmpty(this.aabb) || CollisionUtil.isEmpty(axisalignedbb)) {
-+ return d0;
-+ }
-+ switch (enumdirection_enumaxis.ordinal()) {
-+ case 0:
-+ return CollisionUtil.collideX(this.aabb, axisalignedbb, d0);
-+ case 1:
-+ return CollisionUtil.collideY(this.aabb, axisalignedbb, d0);
-+ case 2:
-+ return CollisionUtil.collideZ(this.aabb, axisalignedbb, d0);
-+ default:
-+ throw new IllegalStateException("Unknown axis requested");
-+ }
-+ }
-+
-+ @Override
-+ public boolean intersects(AABB axisalingedbb) {
-+ return CollisionUtil.voxelShapeIntersect(this.aabb, axisalingedbb);
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ad711b6c0628a9cd93ff0d5484769807e5e5b9c0
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
-@@ -0,0 +1,500 @@
-+package com.tuinity.tuinity.world;
-+
-+import com.destroystokyo.paper.util.maplist.EntityList;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
-+import net.minecraft.server.level.ChunkHolder;
-+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.phys.AABB;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.function.Predicate;
-+
-+public final class ChunkEntitySlices {
-+
-+ protected final int minSection;
-+ protected final int maxSection;
-+ protected final int chunkX;
-+ protected final int chunkZ;
-+ protected final ServerLevel world;
-+
-+ protected final EntityCollectionBySection allEntities;
-+ protected final EntityCollectionBySection hardCollidingEntities;
-+ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass;
-+ protected final EntityList entities = new EntityList();
-+
-+ public ChunkHolder.FullChunkStatus status;
-+
-+ // TODO implement container search optimisations
-+
-+ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.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.status = status;
-+ }
-+
-+ // Tuinity start - optimise CraftChunk#getEntities
-+ 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]);
-+ }
-+ // Tuinity end - optimise CraftChunk#getEntities
-+
-+ public boolean isEmpty() {
-+ return this.entities.size() == 0;
-+ }
-+
-+ private void updateTicketLevels() {
-+ 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];
-+ entity.chunkStatus = this.status;
-+ }
-+ }
-+
-+ public synchronized void updateStatus(final ChunkHolder.FullChunkStatus status) {
-+ this.status = status;
-+ this.updateTicketLevels();
-+ }
-+
-+ public synchronized void addEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.add(entity)) {
-+ return;
-+ }
-+ entity.chunkStatus = this.status;
-+ final int sectionIndex = chunkSection - this.minSection;
-+
-+ this.allEntities.addEntity(entity, sectionIndex);
-+
-+ if (entity.hardCollides()) {
-+ 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);
-+ }
-+ }
-+ }
-+
-+ public synchronized void removeEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.remove(entity)) {
-+ return;
-+ }
-+ entity.chunkStatus = ChunkHolder.FullChunkStatus.INACCESSIBLE;
-+ final int sectionIndex = chunkSection - this.minSection;
-+
-+ this.allEntities.removeEntity(entity, sectionIndex);
-+
-+ if (entity.hardCollides()) {
-+ 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);
-+ }
-+ }
-+ }
-+
-+ 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 getEntities(final EntityType> type, final AABB box, final List super T> into,
-+ final Predicate super T> predicate) {
-+ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate);
-+ }
-+
-+ 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 {
-+ synchronized (this) {
-+ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz));
-+ }
-+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
-+ }
-+ }
-+
-+ public synchronized void updateEntity(final Entity entity) {
-+ /*// TODO
-+ if (prev aabb != entity.getBoundingBox()) {
-+ this.entityMap.delete(entity, prev aabb);
-+ this.entityMap.insert(entity, prev aabb = entity.getBoundingBox());
-+ }*/
-+ }
-+
-+ protected static final class BasicEntityList {
-+
-+ protected static final Entity[] EMPTY = new Entity[0];
-+ protected static final int DEFAULT_CAPACITY = 4;
-+
-+ protected E[] storage;
-+ protected 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;
-+ }
-+ }
-+
-+ protected static final class EntityCollectionBySection {
-+
-+ protected final ChunkEntitySlices manager;
-+ protected final long[] nonEmptyBitset;
-+ protected final BasicEntityList[] entitiesBySection;
-+ protected int count;
-+
-+ public EntityCollectionBySection(final ChunkEntitySlices manager) {
-+ this.manager = manager;
-+
-+ final int sectionCount = manager.maxSection - manager.minSection + 1;
-+
-+ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE
-+ 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<>();
-+ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1)));
-+ }
-+
-+ 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;
-+ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1)));
-+ }
-+ }
-+
-+ 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.manager.minSection;
-+ final int maxSection = this.manager.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);
-+
-+ // TODO use the bitset
-+
-+ 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 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.manager.minSection;
-+ final int maxSection = this.manager.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);
-+
-+ // TODO use the bitset
-+
-+ 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).subEntities) {
-+ if (part == except || !part.getBoundingBox().intersects(box)) {
-+ continue;
-+ }
-+
-+ if (predicate != null && !predicate.test(part)) {
-+ continue;
-+ }
-+
-+ into.add(part);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ 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.manager.minSection;
-+ final int maxSection = this.manager.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);
-+
-+ // TODO use the bitset
-+
-+ 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).subEntities) {
-+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
-+ continue;
-+ }
-+
-+ if (predicate != null && !predicate.test(part)) {
-+ continue;
-+ }
-+
-+ into.add(part);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public void getEntities(final EntityType> type, final AABB box, final List super T> into,
-+ final Predicate super T> predicate) {
-+ if (this.count == 0) {
-+ return;
-+ }
-+
-+ final int minSection = this.manager.minSection;
-+ final int maxSection = this.manager.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);
-+
-+ // TODO use the bitset
-+
-+ 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 || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) {
-+ continue;
-+ }
-+
-+ if (predicate != null && !predicate.test((T)entity)) {
-+ continue;
-+ }
-+
-+ into.add((T)entity);
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f188ff6b08abddd06a3120fb15825e0f71196893
---- /dev/null
-+++ b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
-@@ -0,0 +1,391 @@
-+package com.tuinity.tuinity.world;
-+
-+import com.tuinity.tuinity.util.CoordinateUtils;
-+import com.tuinity.tuinity.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ChunkHolder;
-+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.phys.AABB;
-+import java.util.List;
-+import java.util.concurrent.locks.StampedLock;
-+import java.util.function.Predicate;
-+
-+public final class EntitySliceManager {
-+
-+ 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 ServerLevel world;
-+
-+ private final StampedLock stateLock = new StampedLock();
-+ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(64, 0.7f);
-+
-+ private final int minSection; // inclusive
-+ private final int maxSection; // inclusive
-+
-+ protected final Long2ObjectOpenHashMap statusMap = new Long2ObjectOpenHashMap<>();
-+ {
-+ this.statusMap.defaultReturnValue(ChunkHolder.FullChunkStatus.INACCESSIBLE);
-+ }
-+
-+ public EntitySliceManager(final ServerLevel world) {
-+ this.world = world;
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+ }
-+
-+ public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) {
-+ if (newStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) {
-+ this.statusMap.remove(CoordinateUtils.getChunkKey(x, z));
-+ } else {
-+ this.statusMap.put(CoordinateUtils.getChunkKey(x, z), newStatus);
-+ final ChunkEntitySlices slices = this.getChunk(x, z);
-+ if (slices != null) {
-+ slices.updateStatus(newStatus);
-+ }
-+ }
-+ }
-+
-+ public synchronized void addEntity(final Entity entity) {
-+ 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;
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
-+ slices.addEntity(entity, sectionY);
-+
-+ entity.sectionX = sectionX;
-+ entity.sectionY = sectionY;
-+ entity.sectionZ = sectionZ;
-+ }
-+
-+ public synchronized void removeEntity(final Entity entity) {
-+ final ChunkEntitySlices slices = this.getChunk(entity.sectionX, entity.sectionZ);
-+ slices.removeEntity(entity, entity.sectionY);
-+ if (slices.isEmpty()) {
-+ this.removeChunk(entity.sectionX, entity.sectionZ);
-+ }
-+ }
-+
-+ public void moveEntity(final Entity entity) {
-+ final BlockPos newPos = entity.blockPosition();
-+ final int newSectionX = newPos.getX() >> 4;
-+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
-+ final int newSectionZ = newPos.getZ() >> 4;
-+
-+ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) {
-+ return;
-+ }
-+
-+ synchronized (this) {
-+ // are we changing chunks?
-+ if (newSectionX != entity.sectionX || newSectionZ != entity.sectionZ) {
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
-+ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ);
-+ synchronized (old) {
-+ old.removeEntity(entity, entity.sectionY);
-+ if (old.isEmpty()) {
-+ this.removeChunk(entity.sectionX, entity.sectionZ);
-+ }
-+ }
-+
-+ synchronized (slices) {
-+ slices.addEntity(entity, newSectionY);
-+
-+ entity.sectionX = newSectionX;
-+ entity.sectionY = newSectionY;
-+ entity.sectionZ = newSectionZ;
-+ }
-+ } else {
-+ final ChunkEntitySlices slices = this.getChunk(newSectionX, newSectionZ);
-+ // same chunk
-+ synchronized (slices) {
-+ slices.removeEntity(entity, entity.sectionY);
-+ slices.addEntity(entity, newSectionY);
-+ }
-+ entity.sectionY = newSectionY;
-+ }
-+ }
-+
-+ }
-+
-+ 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(ChunkHolder.FullChunkStatus.BORDER)) {
-+ 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(ChunkHolder.FullChunkStatus.BORDER)) {
-+ 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(ChunkHolder.FullChunkStatus.BORDER)) {
-+ 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(ChunkHolder.FullChunkStatus.BORDER)) {
-+ continue;
-+ }
-+
-+ chunk.getEntities(clazz, except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ 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) {
-+ ret = new ChunkEntitySlices(this.world, chunkX, chunkZ, this.statusMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)),
-+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
-+
-+ this.addChunk(chunkX, chunkZ, ret);
-+
-+ return ret;
-+ }
-+
-+ return ret;
-+ }
-+
-+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
-+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
-+ final long attempt = this.stateLock.tryOptimisticRead();
-+ if (attempt != 0L) {
-+ try {
-+ final ChunkSlicesRegion ret = this.regions.get(key);
-+
-+ if (this.stateLock.validate(attempt)) {
-+ return ret;
-+ }
-+ } catch (final Error error) {
-+ throw error;
-+ } catch (final Throwable thr) {
-+ // ignore
-+ }
-+ }
-+
-+ this.stateLock.readLock();
-+ try {
-+ return this.regions.get(key);
-+ } finally {
-+ this.stateLock.tryUnlockRead();
-+ }
-+ }
-+
-+ public 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.stateLock.writeLock();
-+ try {
-+ this.regions.remove(key);
-+ } finally {
-+ this.stateLock.tryUnlockWrite();
-+ }
-+ }
-+ }
-+
-+ 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.stateLock.writeLock();
-+ try {
-+ this.regions.put(key, region);
-+ } finally {
-+ this.stateLock.tryUnlockWrite();
-+ }
-+ }
-+ }
-+
-+ public static final class ChunkSlicesRegion {
-+
-+ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
-+ protected 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;
-+ }
-+ }
-+}
-diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
-index b70aa66732fb5e957aed0901f4c76358b2c56f8e..b01d7da333bac7820e42b6f645634a15ef88ae4f 100644
---- a/src/main/java/net/minecraft/core/BlockPos.java
-+++ b/src/main/java/net/minecraft/core/BlockPos.java
-@@ -478,9 +478,9 @@ public class BlockPos extends Vec3i {
- }
-
- public BlockPos.MutableBlockPos set(int x, int y, int z) {
-- this.setX(x);
-- this.setY(y);
-- this.setZ(z);
-+ this.x = x; // Tuinity - force inline
-+ this.y = y; // Tuinity - force inline
-+ this.z = z; // Tuinity - force inline
- return this;
- }
-
-@@ -544,19 +544,19 @@ public class BlockPos extends Vec3i {
- // Paper start - comment out useless overrides @Override - TODO figure out why this is suddenly important to keep
- @Override
- public BlockPos.MutableBlockPos setX(int i) {
-- super.setX(i);
-+ this.x = i; // Tuinity
- return this;
- }
-
- @Override
- public BlockPos.MutableBlockPos setY(int i) {
-- super.setY(i);
-+ this.y = i; // Tuinity
- return this;
- }
-
- @Override
- public BlockPos.MutableBlockPos setZ(int i) {
-- super.setZ(i);
-+ this.z = i; // Tuinity
- return this;
- }
- // Paper end
-diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java
-index 5e09890ba2fe326503a49b2dbec09845f5c8c5eb..3ad3652f8074de10222fb01c50548b4312103cc3 100644
---- a/src/main/java/net/minecraft/core/Vec3i.java
-+++ b/src/main/java/net/minecraft/core/Vec3i.java
-@@ -17,9 +17,9 @@ public class Vec3i implements Comparable {
- return IntStream.of(vec3i.getX(), vec3i.getY(), vec3i.getZ());
- });
- public static final Vec3i ZERO = new Vec3i(0, 0, 0);
-- private int x;
-- private int y;
-- private int z;
-+ protected int x; // Tuinity - protected
-+ protected int y; // Tuinity - protected
-+ protected int z; // Tuinity - protected
-
- // Paper start
- public boolean isValidLocation(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) {
-@@ -84,17 +84,17 @@ public class Vec3i implements Comparable {
- return this.z;
- }
-
-- public Vec3i setX(int x) {
-+ protected Vec3i setX(int x) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
- this.x = x;
- return this;
- }
-
-- public Vec3i setY(int y) {
-+ protected Vec3i setY(int y) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
- this.y = y;
- return this;
- }
-
-- public Vec3i setZ(int z) {
-+ protected Vec3i setZ(int z) { // Tuinity - not needed here - Also revert the decision to expose set on an _immutable_ type
- this.z = z;
- return this;
- }
-diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
-index c203a78a28e6457bd25b34b5c5ecaa35e3f9211e..0232fb8123c7dfa735802442f8575c6ce1566847 100644
---- a/src/main/java/net/minecraft/network/Connection.java
-+++ b/src/main/java/net/minecraft/network/Connection.java
-@@ -49,6 +49,8 @@ import org.apache.logging.log4j.Logger;
- import org.apache.logging.log4j.Marker;
- import org.apache.logging.log4j.MarkerManager;
-
-+
-+import io.netty.util.concurrent.AbstractEventExecutor; // Tuinity
- public class Connection extends SimpleChannelInboundHandler> {
-
- private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F;
-@@ -93,6 +95,77 @@ public class Connection extends SimpleChannelInboundHandler> {
- public boolean queueImmunity = false;
- public ConnectionProtocol protocol;
- // Paper end
-+ // Tuinity start - add pending task queue
-+ private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
-+ public void execute(final Runnable run) {
-+ if (this.channel == null || !this.channel.isRegistered()) {
-+ run.run();
-+ return;
-+ }
-+ final boolean queue = !this.queue.isEmpty();
-+ if (!queue) {
-+ this.channel.eventLoop().execute(run);
-+ } else {
-+ this.pendingTasks.add(run);
-+ if (this.queue.isEmpty()) {
-+ // something flushed async, dump tasks now
-+ Runnable r;
-+ while ((r = this.pendingTasks.poll()) != null) {
-+ this.channel.eventLoop().execute(r);
-+ }
-+ }
-+ }
-+ }
-+ // Tuinity end - add pending task queue
-+
-+ // Tuinity start - allow controlled flushing
-+ volatile boolean canFlush = true;
-+ private final java.util.concurrent.atomic.AtomicInteger packetWrites = new java.util.concurrent.atomic.AtomicInteger();
-+ private int flushPacketsStart;
-+ private final Object flushLock = new Object();
-+
-+ public void disableAutomaticFlush() {
-+ synchronized (this.flushLock) {
-+ this.flushPacketsStart = this.packetWrites.get(); // must be volatile and before canFlush = false
-+ this.canFlush = false;
-+ }
-+ }
-+
-+ public void enableAutomaticFlush() {
-+ synchronized (this.flushLock) {
-+ this.canFlush = true;
-+ if (this.packetWrites.get() != this.flushPacketsStart) { // must be after canFlush = true
-+ this.flush(); // only make the flush call if we need to
-+ }
-+ }
-+ }
-+
-+ private final void flush() {
-+ if (this.channel.eventLoop().inEventLoop()) {
-+ this.channel.flush();
-+ } else {
-+ this.channel.eventLoop().execute(() -> {
-+ this.channel.flush();
-+ });
-+ }
-+ }
-+ // Tuinity end - allow controlled flushing
-+ // Tuinity start - packet limiter
-+ protected final Object PACKET_LIMIT_LOCK = new Object();
-+ protected final com.tuinity.tuinity.util.IntervalledCounter allPacketCounts = com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit != null ? new com.tuinity.tuinity.util.IntervalledCounter(
-+ (long)(com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.packetLimitInterval * 1.0e9)
-+ ) : null;
-+ protected final java.util.Map>, com.tuinity.tuinity.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
-+
-+ private boolean stopReadingPackets;
-+ private void killForPacketSpam() {
-+ this.sendPacket(new ClientboundDisconnectPacket(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]), (future) -> {
-+ this.disconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]);
-+ });
-+ this.setReadOnly();
-+ this.stopReadingPackets = true;
-+ }
-+ // Tuinity end - packet limiter
-
- public Connection(PacketFlow side) {
- this.receiving = side;
-@@ -173,6 +246,45 @@ public class Connection extends SimpleChannelInboundHandler> {
-
- protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet> packet) {
- if (this.channel.isOpen()) {
-+ // Tuinity start - packet limiter
-+ if (this.stopReadingPackets) {
-+ return;
-+ }
-+ if (this.allPacketCounts != null ||
-+ com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.containsKey(packet.getClass())) {
-+ long time = System.nanoTime();
-+ synchronized (PACKET_LIMIT_LOCK) {
-+ if (this.allPacketCounts != null) {
-+ this.allPacketCounts.updateAndAdd(1, time);
-+ if (this.allPacketCounts.getRate() >= com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.maxPacketRate) {
-+ this.killForPacketSpam();
-+ return;
-+ }
-+ }
-+
-+ for (Class> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
-+ com.tuinity.tuinity.config.TuinityConfig.PacketLimit packetSpecificLimit =
-+ com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.get(check);
-+ if (packetSpecificLimit == null) {
-+ continue;
-+ }
-+ com.tuinity.tuinity.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
-+ return new com.tuinity.tuinity.util.IntervalledCounter((long)(packetSpecificLimit.packetLimitInterval * 1.0e9));
-+ });
-+ counter.updateAndAdd(1, time);
-+ if (counter.getRate() >= packetSpecificLimit.maxPacketRate) {
-+ switch (packetSpecificLimit.violateAction) {
-+ case DROP:
-+ return;
-+ case KICK:
-+ this.killForPacketSpam();
-+ return;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ // Tuinity end - packet limiter
- try {
- Connection.genericsFtw(packet, this.packetListener);
- } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
-@@ -255,7 +367,7 @@ public class Connection extends SimpleChannelInboundHandler> {
- net.minecraft.server.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() &&
- (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
- ))) {
-- this.sendPacket(packet, callback);
-+ this.writePacket(packet, callback, null); // Tuinity
- return;
- }
- // write the packets to the queue, then flush - antixray hooks there already
-@@ -279,6 +391,14 @@ public class Connection extends SimpleChannelInboundHandler> {
- }
-
- private void sendPacket(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> callback) {
-+ // Tuinity start - add flush parameter
-+ this.writePacket(packet, callback, Boolean.TRUE);
-+ }
-+ private void writePacket(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> callback, Boolean flushConditional) {
-+ this.packetWrites.getAndIncrement(); // must be befeore using canFlush
-+ boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue();
-+ final boolean flush = effectiveFlush || packet instanceof net.minecraft.network.protocol.game.ClientboundKeepAlivePacket || packet instanceof ClientboundDisconnectPacket; // no delay for certain packets
-+ // Tuinity end - add flush parameter
- ConnectionProtocol enumprotocol = ConnectionProtocol.getProtocolForPacket(packet);
- ConnectionProtocol enumprotocol1 = this.getCurrentProtocol();
-
-@@ -289,16 +409,31 @@ public class Connection extends SimpleChannelInboundHandler> {
- }
-
- if (this.channel.eventLoop().inEventLoop()) {
-- this.a(packet, callback, enumprotocol, enumprotocol1);
-+ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter
- } else {
-+ // Tuinity start - optimise packets that are not flushed
-+ // note: since the type is not dynamic here, we need to actually copy the old executor code
-+ // into two branches. On conflict, just re-copy - no changes were made inside the executor code.
-+ if (!flush) {
-+ AbstractEventExecutor.LazyRunnable run = () -> {
-+ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter
-+ };
-+ this.channel.eventLoop().execute(run);
-+ } else { // Tuinity end - optimise packets that are not flushed
- this.channel.eventLoop().execute(() -> {
-- this.a(packet, callback, enumprotocol, enumprotocol1);
-+ this.a(packet, callback, enumprotocol, enumprotocol1, flush); // Tuinity - add flush parameter // Tuinity - diff on change
- });
-+ } // Tuinity
- }
-
- }
-
- private void a(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1) {
-+ // Tuinity start - add flush parameter
-+ this.a(packet, genericfuturelistener, enumprotocol, enumprotocol1, true);
-+ }
-+ private void a(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> genericfuturelistener, ConnectionProtocol enumprotocol, ConnectionProtocol enumprotocol1, boolean flush) {
-+ // Tuinity end - add flush parameter
- if (enumprotocol != enumprotocol1) {
- this.setProtocol(enumprotocol);
- }
-@@ -312,7 +447,7 @@ public class Connection extends SimpleChannelInboundHandler> {
-
- try {
- // Paper end
-- ChannelFuture channelfuture = this.channel.writeAndFlush(packet);
-+ ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter
-
- if (genericfuturelistener != null) {
- channelfuture.addListener(genericfuturelistener);
-@@ -353,7 +488,12 @@ public class Connection extends SimpleChannelInboundHandler> {
- return false;
- }
- private boolean processQueue() {
-+ try { // Tuinity - add pending task queue
- if (this.queue.isEmpty()) return true;
-+ // Tuinity start - make only one flush call per sendPacketQueue() call
-+ final boolean needsFlush = this.canFlush;
-+ boolean hasWrotePacket = false;
-+ // Tuinity end - make only one flush call per sendPacketQueue() call
- // If we are on main, we are safe here in that nothing else should be processing queue off main anymore
- // But if we are not on main due to login/status, the parent is synchronized on packetQueue
- java.util.Iterator iterator = this.queue.iterator();
-@@ -361,19 +501,31 @@ public class Connection extends SimpleChannelInboundHandler> {
- PacketHolder queued = iterator.next(); // poll -> peek
-
- // Fix NPE (Spigot bug caused by handleDisconnection())
-- if (queued == null) {
-+ if (false && queued == null) { // Tuinity - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here
- return true;
- }
-
- Packet> packet = queued.packet;
- if (!packet.isReady()) {
-+ // Tuinity start - make only one flush call per sendPacketQueue() call
-+ if (hasWrotePacket && (needsFlush || this.canFlush)) {
-+ this.flush();
-+ }
-+ // Tuinity end - make only one flush call per sendPacketQueue() call
- return false;
- } else {
- iterator.remove();
-- this.sendPacket(packet, queued.listener);
-+ this.writePacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Tuinity - make only one flush call per sendPacketQueue() call
-+ hasWrotePacket = true; // Tuinity - make only one flush call per sendPacketQueue() call
- }
- }
- return true;
-+ } finally { // Tuinity start - add pending task queue
-+ Runnable r;
-+ while ((r = this.pendingTasks.poll()) != null) {
-+ this.channel.eventLoop().execute(r);
-+ }
-+ } // Tuinity end - add pending task queue
- }
- // Paper end
-
-@@ -396,7 +548,14 @@ public class Connection extends SimpleChannelInboundHandler> {
- }
-
- if (this.packetListener instanceof ServerGamePacketListenerImpl) {
-+ // Tuinity start - detailed watchdog information
-+ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
-+ try {
-+ // Tuinity end - detailed watchdog information
- ((ServerGamePacketListenerImpl) this.packetListener).tick();
-+ } finally { // Tuinity start - detailed watchdog information
-+ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
-+ } // Tuinity start - detailed watchdog information
- }
-
- if (!this.isConnected() && !this.disconnectionHandled) {
-diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-index bcf53ec07b8eeec7a88fb67e6fb908362e6f51b0..7265bee436d61d33645fa2d9ed4240529834dbf5 100644
---- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
-@@ -20,6 +20,24 @@ public class PacketUtils {
-
- private static final Logger LOGGER = LogManager.getLogger();
-
-+ // Tuinity start - detailed watchdog information
-+ public static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>();
-+ static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong();
-+
-+ public static long getTotalProcessedPackets() {
-+ return totalMainThreadPacketsProcessed.get();
-+ }
-+
-+ public static java.util.List getCurrentPacketProcessors() {
-+ java.util.List ret = new java.util.ArrayList<>(4);
-+ for (PacketListener listener : packetProcessing) {
-+ ret.add(listener);
-+ }
-+
-+ return ret;
-+ }
-+ // Tuinity end - detailed watchdog information
-+
- public PacketUtils() {}
-
- public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException {
-@@ -30,6 +48,8 @@ public class PacketUtils {
- if (!engine.isSameThread()) {
- Timing timing = MinecraftTimings.getPacketTiming(packet); // Paper - timings
- engine.execute(() -> {
-+ packetProcessing.push(listener); // Tuinity - detailed watchdog information
-+ try { // Tuinity - detailed watchdog information
- if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590
- if (listener.getConnection().isConnected()) {
- try (Timing ignored = timing.startTiming()) { // Paper - timings
-@@ -53,6 +73,12 @@ public class PacketUtils {
- } else {
- PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet);
- }
-+ // Tuinity start - detailed watchdog information
-+ } finally {
-+ totalMainThreadPacketsProcessed.getAndIncrement();
-+ packetProcessing.pop();
-+ }
-+ // Tuinity end - detailed watchdog information
-
- });
- throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
-diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
-index d8be2ad889f46491e50404916fb4ae0de5f42098..5b9ea0af272c5e7a85d2a954a9214bf875bc7e9f 100644
---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
-+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
-@@ -32,25 +32,17 @@ public class ClientboundLightUpdatePacket implements Packet {
-- if (remainingSends.get() == 0) {
-- cleaner1.run();
-- cleaner2.run();
-- }
-- }, "Light Packet Release");
-- }
-+ // Tuinity - rewrite light engine
- }
-
- @Override
- public boolean hasFinishListener() {
-- return true;
-+ return false; // Tuinity - rewrite light engine
- }
-
- // Paper end
-@@ -63,8 +55,8 @@ public class ClientboundLightUpdatePacket implements Packet com.mojang.serialization.MapCodec fieldWithFallbacks(com.mojang.serialization.Codec codec, String name, String ...fallback) {
-+ return com.mojang.serialization.MapCodec.of(
-+ new com.mojang.serialization.codecs.FieldEncoder<>(name, codec),
-+ new FieldFallbackDecoder<>(name, java.util.Arrays.asList(fallback), codec),
-+ () -> "FieldFallback[" + name + ": " + codec.toString() + "]"
-+ );
-+ }
-+
-+ // This is likely a common occurrence, sadly
-+ public static final class FieldFallbackDecoder extends com.mojang.serialization.MapDecoder.Implementation {
-+ protected final String name;
-+ protected final List fallback;
-+ private final com.mojang.serialization.Decoder elementCodec;
-+
-+ public FieldFallbackDecoder(final String name, final List