From cc8d4390d4fa8f83e54b1792f1f6ee3e615ee8a1 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Wed, 17 Jul 2024 10:24:53 -0700
Subject: [PATCH] Remove Moonrise utils to MCUtils, remove duplicated/unused
 utils

---
 patches/server/Add-API-for-quit-reason.patch  |    2 +-
 ...-Plugin-Tickets-to-API-Chunk-Methods.patch |    2 +-
 patches/server/Add-TickThread.patch           |  109 -
 patches/server/Brand-support.patch            |    2 +-
 ...l-more-information-in-watchdog-dumps.patch |    8 +-
 .../Fix-CraftWorld-isChunkGenerated.patch     |    2 +-
 ...k-event-leave-message-not-being-sent.patch |    2 +-
 ...Folia-scheduler-and-owned-region-API.patch |   16 +-
 patches/server/Implement-Mob-Goal-API.patch   |   13 +-
 .../Implement-Player-Client-Options-API.patch |    2 +-
 .../Improve-and-expand-AsyncCatcher.patch     |    2 +-
 ...ng-PreCreatureSpawnEvent-with-per-pl.patch |   10 +-
 .../server/Improved-Watchdog-Support.patch    |   10 +-
 .../Incremental-chunk-and-player-saving.patch |    2 +-
 patches/server/Lag-compensation-ticks.patch   |    2 +-
 patches/server/MC-Utils.patch                 | 5227 +++++++++--------
 .../Moonrise-optimisation-patches.patch       | 4568 ++------------
 .../server/Optimise-general-POI-access.patch  |    2 +
 ...oalSelector-Goal.Flag-Set-operations.patch |   24 +-
 .../Optional-per-player-mob-spawns.patch      |   18 +-
 .../PlayerNaturallySpawnCreaturesEvent.patch  |    2 +-
 ...rovide-E-TE-Chunk-count-stat-methods.patch |    4 +-
 patches/server/Timings-v2.patch               |    1 -
 23 files changed, 3563 insertions(+), 6467 deletions(-)
 delete mode 100644 patches/server/Add-TickThread.patch

diff --git a/patches/server/Add-API-for-quit-reason.patch b/patches/server/Add-API-for-quit-reason.patch
index 830827f363..9522d01639 100644
--- a/patches/server/Add-API-for-quit-reason.patch
+++ b/patches/server/Add-API-for-quit-reason.patch
@@ -32,7 +32,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
 +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
 @@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
-     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
+     public boolean isRealPlayer; // Paper
      public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
      public @Nullable String clientBrandName = null; // Paper - Brand support
 +    public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
diff --git a/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch b/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
index 9b7fa1881e..e155198901 100644
--- a/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
+++ b/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
@@ -94,7 +94,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
 @@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-         io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
+         ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
              net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
                  net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
 +                if (chunk != null) this.addTicket(x, z); // Paper
diff --git a/patches/server/Add-TickThread.patch b/patches/server/Add-TickThread.patch
deleted file mode 100644
index 40e3068554..0000000000
--- a/patches/server/Add-TickThread.patch
+++ /dev/null
@@ -1,109 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Sun, 3 Mar 2019 20:53:18 -0800
-Subject: [PATCH] Add TickThread
-
-Placeholder patch, to be used by chunksystem rewrite
-
-diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/TickThread.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
-+
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.entity.Entity;
-+import org.bukkit.Bukkit;
-+import java.util.concurrent.atomic.AtomicInteger;
-+
-+public final class TickThread extends Thread {
-+
-+    public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.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 (!isTickThread()) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+    public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
-+        if (!isTickThreadFor(world, chunkX, chunkZ)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+    public static void ensureTickThread(final Entity entity, final String reason) {
-+        if (!isTickThreadFor(entity)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+    public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
-+
-+    private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
-+
-+    public TickThread(final String name) {
-+        this(null, name);
-+    }
-+
-+    public TickThread(final Runnable run, final String name) {
-+        this(run, name, ID_GENERATOR.incrementAndGet());
-+    }
-+
-+    private TickThread(final Runnable run, final String name, final int id) {
-+        super(run, name);
-+        this.id = id;
-+    }
-+
-+    public static TickThread getCurrentTickThread() {
-+        return (TickThread) Thread.currentThread();
-+    }
-+
-+    public static boolean isTickThread() {
-+        return Bukkit.isPrimaryThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final Entity entity) {
-+        return isTickThread();
-+    }
-+}
-diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/spigotmc/AsyncCatcher.java
-+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
-@@ -0,0 +0,0 @@ public class AsyncCatcher
- 
-     public static void catchOp(String reason)
-     {
--        if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
-+        if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
-         {
-             throw new IllegalStateException( "Asynchronous " + reason + "!" );
-         }
diff --git a/patches/server/Brand-support.patch b/patches/server/Brand-support.patch
index be96476778..db8d5b789f 100644
--- a/patches/server/Brand-support.patch
+++ b/patches/server/Brand-support.patch
@@ -9,8 +9,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
 +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
 @@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+     // CraftBukkit end
      public boolean isRealPlayer; // Paper
-     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
      public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
 +    public @Nullable String clientBrandName = null; // Paper - Brand support
  
diff --git a/patches/server/Detail-more-information-in-watchdog-dumps.patch b/patches/server/Detail-more-information-in-watchdog-dumps.patch
index 268f947057..05098f3628 100644
--- a/patches/server/Detail-more-information-in-watchdog-dumps.patch
+++ b/patches/server/Detail-more-information-in-watchdog-dumps.patch
@@ -97,7 +97,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
      public void tickNonPassenger(Entity entity) {
 +        // Paper start - log detailed entity tick information
-+        io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
 +        try {
 +            if (currentlyTickingEntity.get() == null) {
 +                currentlyTickingEntity.lazySet(entity);
@@ -157,7 +157,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public void move(MoverType movementType, Vec3 movement) {
          final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity
 +        // Paper start - detailed watchdog information
-+        io.papermc.paper.util.TickThread.ensureTickThread("Cannot move an entity off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot move an entity off-main");
 +        synchronized (this.posLock) {
 +            this.moveStartX = this.getX();
 +            this.moveStartY = this.getY();
@@ -207,7 +207,7 @@ diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/spigotmc/WatchdogThread.java
 +++ b/src/main/java/org/spigotmc/WatchdogThread.java
-@@ -0,0 +0,0 @@ public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper -
+@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
      private volatile long lastTick;
      private volatile boolean stopping;
  
@@ -286,7 +286,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private WatchdogThread(long timeoutTime, boolean restart)
      {
          super( "Paper Watchdog Thread" );
-@@ -0,0 +0,0 @@ public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper -
+@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
                  log.log( Level.SEVERE, "------------------------------" );
                  log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
                  ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
diff --git a/patches/server/Fix-CraftWorld-isChunkGenerated.patch b/patches/server/Fix-CraftWorld-isChunkGenerated.patch
index b3fb7643c4..06c95139ff 100644
--- a/patches/server/Fix-CraftWorld-isChunkGenerated.patch
+++ b/patches/server/Fix-CraftWorld-isChunkGenerated.patch
@@ -28,7 +28,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk;
          }
 +        final java.util.concurrent.CompletableFuture<ChunkAccess> future = new java.util.concurrent.CompletableFuture<>();
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(
++        ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
 +            this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, future::complete
 +        );
 +        world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone);
diff --git a/patches/server/Fix-kick-event-leave-message-not-being-sent.patch b/patches/server/Fix-kick-event-leave-message-not-being-sent.patch
index 37895d91ab..81d08f74e8 100644
--- a/patches/server/Fix-kick-event-leave-message-not-being-sent.patch
+++ b/patches/server/Fix-kick-event-leave-message-not-being-sent.patch
@@ -15,7 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -    public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
      // CraftBukkit end
      public boolean isRealPlayer; // Paper
-     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
+     public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
 diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
diff --git a/patches/server/Folia-scheduler-and-owned-region-API.patch b/patches/server/Folia-scheduler-and-owned-region-API.patch
index f0fc21fba3..c8146e25c4 100644
--- a/patches/server/Folia-scheduler-and-owned-region-API.patch
+++ b/patches/server/Folia-scheduler-and-owned-region-API.patch
@@ -52,7 +52,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package io.papermc.paper.threadedregions;
 +
 +import ca.spottedleaf.concurrentutil.util.Validate;
-+import io.papermc.paper.util.TickThread;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 +import net.minecraft.world.entity.Entity;
 +import org.bukkit.craftbukkit.entity.CraftEntity;
@@ -1280,14 +1280,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position) {
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4
 +        );
 +    }
 +
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position, int squareRadiusChunks) {
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4, squareRadiusChunks
 +        );
 +    }
@@ -1295,7 +1295,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(Location location) {
 +        World world = location.getWorld();
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4
 +        );
 +    }
@@ -1303,28 +1303,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(Location location, int squareRadiusChunks) {
 +        World world = location.getWorld();
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4, squareRadiusChunks
 +        );
 +    }
 +
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) {
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), chunkX, chunkZ
 +        );
 +    }
 +
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ, int squareRadiusChunks) {
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(
 +            ((CraftWorld) world).getHandle(), chunkX, chunkZ, squareRadiusChunks
 +        );
 +    }
 +
 +    @Override
 +    public final boolean isOwnedByCurrentRegion(Entity entity) {
-+        return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandleRaw());
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandleRaw());
 +    }
 +    // Paper end - Folia reagion threading API
 +
diff --git a/patches/server/Implement-Mob-Goal-API.patch b/patches/server/Implement-Mob-Goal-API.patch
index 2f4ba1229c..cc8002fe86 100644
--- a/patches/server/Implement-Mob-Goal-API.patch
+++ b/patches/server/Implement-Mob-Goal-API.patch
@@ -25,7 +25,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package com.destroystokyo.paper.entity.ai;
 +
 +import com.destroystokyo.paper.entity.RangedEntity;
-+import com.destroystokyo.paper.util.set.OptimizedSmallEnumSet;
 +import com.google.common.collect.BiMap;
 +import com.google.common.collect.HashBiMap;
 +import io.papermc.paper.util.ObfHelper;
@@ -316,7 +315,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static EnumSet<GoalType> vanillaToPaper(Goal goal) {
 +        EnumSet<GoalType> goals = EnumSet.noneOf(GoalType.class);
 +        for (GoalType type : GoalType.values()) {
-+            if (goal.getFlags().contains(paperToVanilla(type))) {
++            if (goal.getFlags().hasElement(paperToVanilla(type))) {
 +                goals.add(type);
 +            }
 +        }
@@ -422,7 +421,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        this.setFlags(MobGoalHelper.paperToVanilla(handle.getTypes()));
 +        if (this.getFlags().size() == 0) {
-+            this.getFlags().add(Flag.UNKNOWN_BEHAVIOR);
++            this.getFlags().addUnchecked(Flag.UNKNOWN_BEHAVIOR);
 +        }
 +    }
 +
@@ -584,7 +583,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        CraftMob craftMob = (CraftMob) mob;
 +        Set<Goal<T>> goals = new HashSet<>();
 +        for (WrappedGoal item : getHandle(craftMob, type).getAvailableGoals()) {
-+            if (!item.getGoal().getFlags().contains(MobGoalHelper.paperToVanilla(type))) {
++            if (!item.getGoal().getFlags().hasElement(MobGoalHelper.paperToVanilla(type))) {
 +                continue;
 +            }
 +
@@ -607,7 +606,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                continue;
 +            }
 +            for (WrappedGoal item : getHandle(craftMob, internalType).getAvailableGoals()) {
-+                if (item.getGoal().getFlags().contains(MobGoalHelper.paperToVanilla(type))) {
++                if (item.getGoal().getFlags().hasElement(MobGoalHelper.paperToVanilla(type))) {
 +                    continue;
 +                }
 +
@@ -637,7 +636,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        Set<Goal<T>> goals = new HashSet<>();
 +        getHandle(craftMob, type).getAvailableGoals()
 +            .stream().filter(WrappedGoal::isRunning)
-+            .filter(item -> item.getGoal().getFlags().contains(MobGoalHelper.paperToVanilla(type)))
++            .filter(item -> item.getGoal().getFlags().hasElement(MobGoalHelper.paperToVanilla(type)))
 +            .forEach(item -> {
 +                if (item.getGoal() instanceof PaperCustomGoal) {
 +                    //noinspection unchecked
@@ -660,7 +659,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            getHandle(craftMob, internalType).getAvailableGoals()
 +                .stream()
 +                .filter(WrappedGoal::isRunning)
-+                .filter(item -> !item.getGoal().getFlags().contains(MobGoalHelper.paperToVanilla(type)))
++                .filter(item -> !item.getGoal().getFlags().hasElement(MobGoalHelper.paperToVanilla(type)))
 +                .forEach(item -> {
 +                    if (item.getGoal() instanceof PaperCustomGoal) {
 +                        //noinspection unchecked
diff --git a/patches/server/Implement-Player-Client-Options-API.patch b/patches/server/Implement-Player-Client-Options-API.patch
index c60c075fc2..9f6c0e5ad8 100644
--- a/patches/server/Implement-Player-Client-Options-API.patch
+++ b/patches/server/Implement-Player-Client-Options-API.patch
@@ -98,7 +98,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.updateOptionsNoEvents(clientOptions); // Paper - don't call options events on login
          this.object = null;
  
-         this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
+         // CraftBukkit start
 @@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
          }
      }
diff --git a/patches/server/Improve-and-expand-AsyncCatcher.patch b/patches/server/Improve-and-expand-AsyncCatcher.patch
index 061be9e938..20c6145862 100644
--- a/patches/server/Improve-and-expand-AsyncCatcher.patch
+++ b/patches/server/Improve-and-expand-AsyncCatcher.patch
@@ -219,7 +219,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +++ b/src/main/java/org/spigotmc/AsyncCatcher.java
 @@ -0,0 +0,0 @@ public class AsyncCatcher
      {
-         if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
+         if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
          {
 +            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
              throw new IllegalStateException( "Asynchronous " + reason + "!" );
diff --git a/patches/server/Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/server/Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
index 7b15aa2350..081001c1f7 100644
--- a/patches/server/Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
+++ b/patches/server/Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
@@ -10,7 +10,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             ++((ServerPlayer)backingSet[i]).mobCounts[index];
+             ++(backingSet[i].mobCounts[index]);
          }
      }
 +    // Paper start - per player mob count backoff
@@ -19,14 +19,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +        int idx = mobCategory.ordinal();
-+        final com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> inRange =
-+            this.getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
++            this.level.moonrise$getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
 +        if (inRange == null) {
 +            return;
 +        }
-+        final Object[] backingSet = inRange.getRawData();
++        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
 +        for (int i = 0, len = inRange.size(); i < len; i++) {
-+            ++((ServerPlayer)backingSet[i]).mobBackoffCounts[idx];
++            ++(backingSet[i].mobBackoffCounts[idx]);
 +        }
 +    }
 +    // Paper end - per player mob count backoff
diff --git a/patches/server/Improved-Watchdog-Support.patch b/patches/server/Improved-Watchdog-Support.patch
index 76abe4409c..1175ad714f 100644
--- a/patches/server/Improved-Watchdog-Support.patch
+++ b/patches/server/Improved-Watchdog-Support.patch
@@ -92,7 +92,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
      public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
          AtomicReference<S> atomicreference = new AtomicReference();
-         Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
+         Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
 @@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
      // CraftBukkit start
      private boolean hasStopped = false;
@@ -352,14 +352,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/spigotmc/WatchdogThread.java
 +++ b/src/main/java/org/spigotmc/WatchdogThread.java
 @@ -0,0 +0,0 @@ import org.bukkit.Bukkit;
- public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system
+ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
  {
  
 +    public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - Improved watchdog support
      private static WatchdogThread instance;
      private long timeoutTime;
      private boolean restart;
-@@ -0,0 +0,0 @@ public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper -
+@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
      {
          if ( WatchdogThread.instance == null )
          {
@@ -367,7 +367,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart );
              WatchdogThread.instance.start();
          } else
-@@ -0,0 +0,0 @@ public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper -
+@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
              // Paper start
              Logger log = Bukkit.getServer().getLogger();
              long currentTime = WatchdogThread.monotonicMillis();
@@ -384,7 +384,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  lastEarlyWarning = currentTime;
                  if (isLongTimeout) {
                  // Paper end
-@@ -0,0 +0,0 @@ public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper -
+@@ -0,0 +0,0 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
  
                  if ( isLongTimeout )
                  {
diff --git a/patches/server/Incremental-chunk-and-player-saving.patch b/patches/server/Incremental-chunk-and-player-saving.patch
index c2f1b05e79..91f76e3917 100644
--- a/patches/server/Incremental-chunk-and-player-saving.patch
+++ b/patches/server/Incremental-chunk-and-player-saving.patch
@@ -52,9 +52,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
 +        this.profiler.pop();
 +        // Paper end - Incremental chunk and player saving
-         io.papermc.paper.util.CachedLists.reset(); // Paper
          // Paper start - move executeAll() into full server tick timing
          try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
+             this.runAllTasks();
 diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java
diff --git a/patches/server/Lag-compensation-ticks.patch b/patches/server/Lag-compensation-ticks.patch
index 6f404d22d2..c3676628b1 100644
--- a/patches/server/Lag-compensation-ticks.patch
+++ b/patches/server/Lag-compensation-ticks.patch
@@ -32,7 +32,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java
 +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
 @@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
-         return this.nearbyPlayers;
+         return this.entityTickingChunks;
      }
      // Paper end - rewrite chunk system
 +    // Paper start - lag compensation
diff --git a/patches/server/MC-Utils.patch b/patches/server/MC-Utils.patch
index 5ca01c1bb5..a9775cc258 100644
--- a/patches/server/MC-Utils.patch
+++ b/patches/server/MC-Utils.patch
@@ -12,147 +12,13 @@ public net.minecraft.server.level.ServerChunkCache mainThreadProcessor
 public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
 public net.minecraft.world.level.chunk.LevelChunkSection states
 
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
 @@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.maplist;
-+
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.NoSuchElementException;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+// list with O(1) remove & contains
-+/**
-+ * @author Spottedleaf
-+ */
-+public final class ChunkList implements Iterable<LevelChunk> {
-+
-+    protected final Long2IntOpenHashMap chunkToIndex = new Long2IntOpenHashMap(2, 0.8f);
-+    {
-+        this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE);
-+    }
-+
-+    protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0];
-+
-+    protected LevelChunk[] chunks = EMPTY_LIST;
-+    protected int count;
-+
-+    public int size() {
-+        return this.count;
-+    }
-+
-+    public boolean contains(final LevelChunk chunk) {
-+        return this.chunkToIndex.containsKey(chunk.coordinateKey);
-+    }
-+
-+    public boolean remove(final LevelChunk chunk) {
-+        final int index = this.chunkToIndex.remove(chunk.coordinateKey);
-+        if (index == Integer.MIN_VALUE) {
-+            return false;
-+        }
-+
-+        // move the entity at the end to this index
-+        final int endIndex = --this.count;
-+        final LevelChunk end = this.chunks[endIndex];
-+        if (index != endIndex) {
-+            // not empty after this call
-+            this.chunkToIndex.put(end.coordinateKey, index); // update index
-+        }
-+        this.chunks[index] = end;
-+        this.chunks[endIndex] = null;
-+
-+        return true;
-+    }
-+
-+    public boolean add(final LevelChunk chunk) {
-+        final int count = this.count;
-+        final int currIndex = this.chunkToIndex.putIfAbsent(chunk.coordinateKey, count);
-+
-+        if (currIndex != Integer.MIN_VALUE) {
-+            return false; // already in this list
-+        }
-+
-+        LevelChunk[] list = this.chunks;
-+
-+        if (list.length == count) {
-+            // resize required
-+            list = this.chunks = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+        }
-+
-+        list[count] = chunk;
-+        this.count = count + 1;
-+
-+        return true;
-+    }
-+
-+    public LevelChunk getChecked(final int index) {
-+        if (index < 0 || index >= this.count) {
-+            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
-+        }
-+        return this.chunks[index];
-+    }
-+
-+    public LevelChunk getUnchecked(final int index) {
-+        return this.chunks[index];
-+    }
-+
-+    public LevelChunk[] getRawData() {
-+        return this.chunks;
-+    }
-+
-+    public void clear() {
-+        this.chunkToIndex.clear();
-+        Arrays.fill(this.chunks, 0, this.count, null);
-+        this.count = 0;
-+    }
-+
-+    @Override
-+    public Iterator<LevelChunk> iterator() {
-+        return new Iterator<LevelChunk>() {
-+
-+            LevelChunk lastRet;
-+            int current;
-+
-+            @Override
-+            public boolean hasNext() {
-+                return this.current < ChunkList.this.count;
-+            }
-+
-+            @Override
-+            public LevelChunk next() {
-+                if (this.current >= ChunkList.this.count) {
-+                    throw new NoSuchElementException();
-+                }
-+                return this.lastRet = ChunkList.this.chunks[this.current++];
-+            }
-+
-+            @Override
-+            public void remove() {
-+                final LevelChunk lastRet = this.lastRet;
-+
-+                if (lastRet == null) {
-+                    throw new IllegalStateException();
-+                }
-+                this.lastRet = null;
-+
-+                ChunkList.this.remove(lastRet);
-+                --this.current;
-+            }
-+        };
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.maplist;
++package ca.spottedleaf.moonrise.common.list;
 +
 +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 +import net.minecraft.world.entity.Entity;
@@ -161,6 +27,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import java.util.NoSuchElementException;
 +
 +// list with O(1) remove & contains
++
 +/**
 + * @author Spottedleaf
 + */
@@ -280,13 +147,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        };
 +    }
 +}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
 @@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.maplist;
++package ca.spottedleaf.moonrise.common.list;
 +
 +import it.unimi.dsi.fastutil.longs.LongIterator;
 +import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
@@ -295,12 +162,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import net.minecraft.world.level.block.state.BlockState;
 +import net.minecraft.world.level.chunk.GlobalPalette;
 +
-+/**
-+ * @author Spottedleaf
-+ */
 +public final class IBlockDataList {
 +
-+    static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
++    private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
 +
 +    // map of location -> (index | (location << 16) | (palette id << 32))
 +    private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
@@ -414,33 +278,365 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.map.values().iterator();
 +    }
 +}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
 @@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.maplist;
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
++
++public final class IteratorSafeOrderedReferenceSet<E> {
++
++    public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
++
++    private final Reference2IntLinkedOpenHashMap<E> indexMap;
++    private int firstInvalidIndex = -1;
++
++    /* list impl */
++    private E[] listElements;
++    private int listSize;
++
++    private final double maxFragFactor;
++
++    private int iteratorCount;
++
++    public IteratorSafeOrderedReferenceSet() {
++        this(16, 0.75f, 16, 0.2);
++    }
++
++    public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++                                           final double maxFragFactor) {
++        this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
++        this.indexMap.defaultReturnValue(-1);
++        this.maxFragFactor = maxFragFactor;
++        this.listElements = (E[])new Object[arrayCapacity];
++    }
++
++    /*
++    public void check() {
++        int iterated = 0;
++        ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
++        if (this.listElements != null) {
++            for (int i = 0; i < this.listSize; ++i) {
++                Object obj = this.listElements[i];
++                if (obj != null) {
++                    iterated++;
++                    if (!check.add((E)obj)) {
++                        throw new IllegalStateException("contains duplicate");
++                    }
++                    if (!this.contains((E)obj)) {
++                        throw new IllegalStateException("desync");
++                    }
++                }
++            }
++        }
++
++        if (iterated != this.size()) {
++            throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++        }
++
++        check.clear();
++        iterated = 0;
++        for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++            final E element = iterator.next();
++            iterated++;
++            if (!check.add(element)) {
++                throw new IllegalStateException("contains duplicate (iterator is wrong)");
++            }
++            if (!this.contains(element)) {
++                throw new IllegalStateException("desync (iterator is wrong)");
++            }
++        }
++
++        if (iterated != this.size()) {
++            throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
++        }
++    }
++    */
++
++    private double getFragFactor() {
++        return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
++    }
++
++    public int createRawIterator() {
++        ++this.iteratorCount;
++        if (this.indexMap.isEmpty()) {
++            return -1;
++        } else {
++            return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++        }
++    }
++
++    public int advanceRawIterator(final int index) {
++        final E[] elements = this.listElements;
++        int ret = index + 1;
++        for (int len = this.listSize; ret < len; ++ret) {
++            if (elements[ret] != null) {
++                return ret;
++            }
++        }
++
++        return -1;
++    }
++
++    public void finishRawIterator() {
++        if (--this.iteratorCount == 0) {
++            if (this.getFragFactor() >= this.maxFragFactor) {
++                this.defrag();
++            }
++        }
++    }
++
++    public boolean remove(final E element) {
++        final int index = this.indexMap.removeInt(element);
++        if (index >= 0) {
++            if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
++                this.firstInvalidIndex = index;
++            }
++            if (this.listElements[index] != element) {
++                throw new IllegalStateException();
++            }
++            this.listElements[index] = null;
++            if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
++                this.defrag();
++            }
++            //this.check();
++            return true;
++        }
++        return false;
++    }
++
++    public boolean contains(final E element) {
++        return this.indexMap.containsKey(element);
++    }
++
++    public boolean add(final E element) {
++        final int listSize = this.listSize;
++
++        final int previous = this.indexMap.putIfAbsent(element, listSize);
++        if (previous != -1) {
++            return false;
++        }
++
++        if (listSize >= this.listElements.length) {
++            this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
++        }
++        this.listElements[listSize] = element;
++        this.listSize = listSize + 1;
++
++        //this.check();
++        return true;
++    }
++
++    private void defrag() {
++        if (this.firstInvalidIndex < 0) {
++            return; // nothing to do
++        }
++
++        if (this.indexMap.isEmpty()) {
++            Arrays.fill(this.listElements, 0, this.listSize, null);
++            this.listSize = 0;
++            this.firstInvalidIndex = -1;
++            //this.check();
++            return;
++        }
++
++        final E[] backingArray = this.listElements;
++
++        int lastValidIndex;
++        java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
++
++        if (this.firstInvalidIndex == 0) {
++            iterator = this.indexMap.reference2IntEntrySet().fastIterator();
++            lastValidIndex = 0;
++        } else {
++            lastValidIndex = this.firstInvalidIndex;
++            final E key = backingArray[lastValidIndex - 1];
++            iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
++                @Override
++                public int getIntValue() {
++                    throw new UnsupportedOperationException();
++                }
++
++                @Override
++                public int setValue(int i) {
++                    throw new UnsupportedOperationException();
++                }
++
++                @Override
++                public E getKey() {
++                    return key;
++                }
++            });
++        }
++
++        while (iterator.hasNext()) {
++            final Reference2IntMap.Entry<E> 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<E> iterator() {
++        return this.iterator(0);
++    }
++
++    public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
++        ++this.iteratorCount;
++        return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++    }
++
++    public java.util.Iterator<E> unsafeIterator() {
++        return this.unsafeIterator(0);
++    }
++    public java.util.Iterator<E> unsafeIterator(final int flags) {
++        return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++    }
++
++    public static interface Iterator<E> extends java.util.Iterator<E> {
++
++        public void finishedIterating();
++
++    }
++
++    private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
++
++        private final IteratorSafeOrderedReferenceSet<E> set;
++        private final boolean canFinish;
++        private final int maxIndex;
++        private int nextIndex;
++        private E pendingValue;
++        private boolean finished;
++        private E lastReturned;
++
++        private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
++            this.set = set;
++            this.canFinish = canFinish;
++            this.maxIndex = maxIndex;
++        }
++
++        @Override
++        public boolean hasNext() {
++            if (this.finished) {
++                return false;
++            }
++            if (this.pendingValue != null) {
++                return true;
++            }
++
++            final E[] elements = this.set.listElements;
++            int index, len;
++            for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
++                final E element = elements[index];
++                if (element != null) {
++                    this.pendingValue = element;
++                    this.nextIndex = index + 1;
++                    return true;
++                }
++            }
++
++            this.nextIndex = index;
++            return false;
++        }
++
++        @Override
++        public E next() {
++            if (!this.hasNext()) {
++                throw new NoSuchElementException();
++            }
++            final E ret = this.pendingValue;
++
++            this.pendingValue = null;
++            this.lastReturned = ret;
++
++            return ret;
++        }
++
++        @Override
++        public void remove() {
++            final E lastReturned = this.lastReturned;
++            if (lastReturned == null) {
++                throw new IllegalStateException();
++            }
++            this.lastReturned = null;
++            this.set.remove(lastReturned);
++        }
++
++        @Override
++        public void finishedIterating() {
++            if (this.finished || !this.canFinish) {
++                throw new IllegalStateException();
++            }
++            this.lastReturned = null;
++            this.finished = true;
++            this.set.finishRawIterator();
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.list;
 +
 +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 +import java.util.Arrays;
 +import java.util.Iterator;
 +import java.util.NoSuchElementException;
 +
-+/**
-+ * @author Spottedleaf
-+ */
 +public final class ReferenceList<E> implements Iterable<E> {
 +
-+    protected final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
-+    {
++    private static final Object[] EMPTY_LIST = new Object[0];
++
++    private final Reference2IntOpenHashMap<E> referenceToIndex;
++    private E[] references;
++    private int count;
++
++    public ReferenceList() {
++        this((E[])EMPTY_LIST);
++    }
++
++    public ReferenceList(final E[] referenceArray) {
++        this.references = referenceArray;
++        this.referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
 +        this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
 +    }
 +
-+    protected static final Object[] EMPTY_LIST = new Object[0];
++    private ReferenceList(final E[] references, final int count, final Reference2IntOpenHashMap<E> referenceToIndex) {
++        this.references = references;
++        this.count = count;
++        this.referenceToIndex = referenceToIndex;
++    }
 +
-+    protected Object[] references = EMPTY_LIST;
-+    protected int count;
++    public ReferenceList<E> copy() {
++        return new ReferenceList<>(this.references.clone(), this.count, this.referenceToIndex.clone());
++    }
 +
 +    public int size() {
 +        return this.count;
@@ -477,7 +673,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return false; // already in this list
 +        }
 +
-+        Object[] list = this.references;
++        E[] list = this.references;
 +
 +        if (list.length == count) {
 +            // resize required
@@ -494,17 +690,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (index < 0 || index >= this.count) {
 +            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
 +        }
-+        return (E)this.references[index];
++        return this.references[index];
 +    }
 +
 +    public E getUnchecked(final int index) {
-+        return (E)this.references[index];
++        return this.references[index];
 +    }
 +
 +    public Object[] getRawData() {
 +        return this.references;
 +    }
 +
++    public E[] getRawDataUnchecked() {
++        return this.references;
++    }
++
 +    public void clear() {
 +        this.referenceToIndex.clear();
 +        Arrays.fill(this.references, 0, this.count, null);
@@ -527,7 +727,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                if (this.current >= ReferenceList.this.count) {
 +                    throw new NoSuchElementException();
 +                }
-+                return this.lastRet = (E)ReferenceList.this.references[this.current++];
++                return this.lastRet = ReferenceList.this.references[this.current++];
 +            }
 +
 +            @Override
@@ -545,305 +745,2028 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        };
 +    }
 +}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
 @@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.misc;
++package ca.spottedleaf.moonrise.common.list;
++
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.Comparator;
++
++public final class SortedList<E> {
++
++    private static final Object[] EMPTY_LIST = new Object[0];
++
++    private Comparator<? super E> comparator;
++    private E[] elements;
++    private int count;
++
++    public SortedList(final Comparator<? super E> comparator) {
++        this((E[])EMPTY_LIST, comparator);
++    }
++
++    public SortedList(final E[] elements, final Comparator<? super E> comparator) {
++        this.elements = elements;
++        this.comparator = comparator;
++    }
++
++    // start, end are inclusive
++    private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> comparator,
++                                     int start, int end) {
++        while (start <= end) {
++            final int middle = (start + end) >>> 1;
++
++            final E middleVal = elements[middle];
++
++            final int cmp = comparator.compare(element, middleVal);
++
++            if (cmp < 0) {
++                end = middle - 1;
++            } else {
++                start = middle + 1;
++            }
++        }
++
++        return start;
++    }
++
++    public int size() {
++        return this.count;
++    }
++
++    public boolean isEmpty() {
++        return this.count == 0;
++    }
++
++    public int add(final E element) {
++        E[] elements = this.elements;
++        final int count = this.count;
++        this.count = count + 1;
++        final Comparator<? super E> comparator = this.comparator;
++
++        final int idx = insertIdx(elements, element, comparator, 0, count - 1);
++
++        if (count >= elements.length) {
++            // copy and insert at the same time
++            if (idx == count) {
++                this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative
++                elements[count] = element;
++                return idx;
++            } else {
++                final E[] newElements = (E[])Array.newInstance(elements.getClass().getComponentType(), (int)Math.max(4L, count * 2L));
++                System.arraycopy(elements, 0, newElements, 0, idx);
++                newElements[idx] = element;
++                System.arraycopy(elements, idx, newElements, idx + 1, count - idx);
++                this.elements = newElements;
++                return idx;
++            }
++        } else {
++            if (idx == count) {
++                // no copy needed
++                elements[idx] = element;
++                return idx;
++            } else {
++                // shift elements down
++                System.arraycopy(elements, idx, elements, idx + 1, count - idx);
++                elements[idx] = element;
++                return idx;
++            }
++        }
++    }
++
++    public E get(final int idx) {
++        if (idx < 0 || idx >= this.count) {
++            throw new IndexOutOfBoundsException(idx);
++        }
++        return this.elements[idx];
++    }
++
++
++    public E remove(final E element) {
++        E[] elements = this.elements;
++        final int count = this.count;
++        final Comparator<? super E> comparator = this.comparator;
++
++        final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
++        if (idx < 0) {
++            return null;
++        }
++
++        final int last = this.count - 1;
++        this.count = last;
++
++        final E ret = elements[idx];
++
++        System.arraycopy(elements, idx + 1, elements, idx, last - idx);
++
++        elements[last] = null;
++
++        return ret;
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.ints.Int2IntFunction;
++
++import java.util.Arrays;
++
++public class Int2IntArraySortedMap {
++
++    protected int[] key;
++    protected int[] val;
++    protected int size;
++
++    public Int2IntArraySortedMap() {
++        this.key = new int[8];
++        this.val = new int[8];
++    }
++
++    public int put(final int key, final int value) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            final int current = this.val[index];
++            this.val[index] = value;
++            return current;
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++        this.val[insert] = value;
++
++        return 0;
++    }
++
++    public int computeIfAbsent(final int key, final Int2IntFunction producer) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            return this.val[index];
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++
++        return this.val[insert] = producer.apply(key);
++    }
++
++    public int get(final int key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            return 0;
++        }
++        return this.val[index];
++    }
++
++    public int getFloor(final int key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            final int insert = -(index + 1) - 1;
++            return insert < 0 ? 0 : this.val[insert];
++        }
++        return this.val[index];
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import java.util.Arrays;
++import java.util.function.IntFunction;
++
++public class Int2ObjectArraySortedMap<V> {
++
++    protected int[] key;
++    protected V[] val;
++    protected int size;
++
++    public Int2ObjectArraySortedMap() {
++        this.key = new int[8];
++        this.val = (V[])new Object[8];
++    }
++
++    public V put(final int key, final V value) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            final V current = this.val[index];
++            this.val[index] = value;
++            return current;
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++
++        this.key[insert] = key;
++        this.val[insert] = value;
++
++        return null;
++    }
++
++    public V computeIfAbsent(final int key, final IntFunction<V> producer) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            return this.val[index];
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++
++        this.key[insert] = key;
++
++        return this.val[insert] = producer.apply(key);
++    }
++
++    public V get(final int key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            return null;
++        }
++        return this.val[index];
++    }
++
++    public V getFloor(final int key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            final int insert = -(index + 1);
++            return this.val[insert];
++        }
++        return this.val[index];
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2IntFunction;
++
++import java.util.Arrays;
++
++public class Long2IntArraySortedMap {
++
++    protected long[] key;
++    protected int[] val;
++    protected int size;
++
++    public Long2IntArraySortedMap() {
++        this.key = new long[8];
++        this.val = new int[8];
++    }
++
++    public int put(final long key, final int value) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            final int current = this.val[index];
++            this.val[index] = value;
++            return current;
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++        this.val[insert] = value;
++
++        return 0;
++    }
++
++    public int computeIfAbsent(final long key, final Long2IntFunction producer) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            return this.val[index];
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++
++        return this.val[insert] = producer.apply(key);
++    }
++
++    public int get(final long key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            return 0;
++        }
++        return this.val[index];
++    }
++
++    public int getFloor(final long key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            final int insert = -(index + 1) - 1;
++            return insert < 0 ? 0 : this.val[insert];
++        }
++        return this.val[index];
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import java.util.Arrays;
++import java.util.function.LongFunction;
++
++public class Long2ObjectArraySortedMap<V> {
++
++    protected long[] key;
++    protected V[] val;
++    protected int size;
++
++    public Long2ObjectArraySortedMap() {
++        this.key = new long[8];
++        this.val = (V[])new Object[8];
++    }
++
++    public V put(final long key, final V value) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            final V current = this.val[index];
++            this.val[index] = value;
++            return current;
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++        this.val[insert] = value;
++
++        return null;
++    }
++
++    public V computeIfAbsent(final long key, final LongFunction<V> producer) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index >= 0) {
++            return this.val[index];
++        }
++        final int insert = -(index + 1);
++        // shift entries down
++        if (this.size >= this.val.length) {
++            this.key = Arrays.copyOf(this.key, this.key.length * 2);
++            this.val = Arrays.copyOf(this.val, this.val.length * 2);
++        }
++        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++        ++this.size;
++
++        this.key[insert] = key;
++
++        return this.val[insert] = producer.apply(key);
++    }
++
++    public V get(final long key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            return null;
++        }
++        return this.val[index];
++    }
++
++    public V getFloor(final long key) {
++        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++        if (index < 0) {
++            final int insert = -(index + 1) - 1;
++            return insert < 0 ? null : this.val[insert];
++        }
++        return this.val[index];
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
++
++import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
++import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
++
++public final class SynchronisedLong2BooleanMap {
++    private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
++    private final int limit;
++
++    public SynchronisedLong2BooleanMap(final int limit) {
++        this.limit = limit;
++    }
++
++    // must hold lock on map
++    private void purgeEntries() {
++        while (this.map.size() > this.limit) {
++            this.map.removeLastBoolean();
++        }
++    }
++
++    public boolean remove(final long key) {
++        synchronized (this.map) {
++            return this.map.remove(key);
++        }
++    }
++
++    // note:
++    public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
++        synchronized (this.map) {
++            if (this.map.containsKey(key)) {
++                return this.map.getAndMoveToFirst(key);
++            }
++        }
++
++        final boolean put = ifAbsent.get(key);
++
++        synchronized (this.map) {
++            if (this.map.containsKey(key)) {
++                return this.map.getAndMoveToFirst(key);
++            }
++            this.map.putAndMoveToFirst(key, put);
++
++            this.purgeEntries();
++
++            return put;
++        }
++    }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.map;
 +
-+import io.papermc.paper.util.IntegerUtil;
 +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.world.level.ChunkPos;
-+import javax.annotation.Nullable;
-+import java.util.Iterator;
++import java.util.function.BiFunction;
 +
-+/** @author Spottedleaf */
-+public abstract class AreaMap<E> {
++public final class SynchronisedLong2ObjectMap<V> {
++    private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
++    private final int limit;
 +
-+    /* Tested via https://gist.github.com/Spottedleaf/520419c6f41ef348fe9926ce674b7217 */
++    public SynchronisedLong2ObjectMap(final int limit) {
++        this.limit = limit;
++    }
 +
-+    protected final Object2LongOpenHashMap<E> objectToLastCoordinate = new Object2LongOpenHashMap<>();
-+    protected final Object2IntOpenHashMap<E> objectToViewDistance = new Object2IntOpenHashMap<>();
++    // must hold lock on map
++    private void purgeEntries() {
++        while (this.map.size() > this.limit) {
++            this.map.removeLast();
++        }
++    }
 +
++    public V get(final long key) {
++        synchronized (this.map) {
++            return this.map.getAndMoveToFirst(key);
++        }
++    }
++
++    public V put(final long key, final V value) {
++        synchronized (this.map) {
++            final V ret = this.map.putAndMoveToFirst(key, value);
++            this.purgeEntries();
++            return ret;
++        }
++    }
++
++    public V compute(final long key, final BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
++        synchronized (this.map) {
++            // first, compute the value - if one is added, it will be at the last entry
++            this.map.compute(key, remappingFunction);
++            // move the entry to first, just in case it was added at last
++            final V ret = this.map.getAndMoveToFirst(key);
++            // now purge the last entries
++            this.purgeEntries();
++
++            return ret;
++        }
++    }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++public final class AllocatingRateLimiter {
++
++    // max difference granularity in ns
++    private final long maxGranularity;
++
++    private double allocation = 0.0;
++    private long lastAllocationUpdate;
++    // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error)
++    // over any time period using take regardless of the number of take calls or the intervals between the take calls
++    // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3
++    private double takeCarry = 0.0;
++    private long lastTakeUpdate;
++
++    public AllocatingRateLimiter(final long maxGranularity) {
++        this.maxGranularity = maxGranularity;
++    }
++
++    public void reset(final long time) {
++        this.allocation = 0.0;
++        this.lastAllocationUpdate = time;
++        this.takeCarry = 0.0;
++        this.lastTakeUpdate = time;
++    }
++
++    // rate in units/s, and time in ns
++    public void tickAllocation(final long time, final double rate, final double maxAllocation) {
++        final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate);
++        this.lastAllocationUpdate = time;
++
++        this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
++    }
++
++    public long previewAllocation(final long time, final double rate, final long maxTake) {
++        if (maxTake < 1L) {
++            return 0L;
++        }
++
++        final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++
++        // note: abs(takeCarry) <= 1.0
++        final double take = Math.min(
++            Math.min((double)maxTake - this.takeCarry, this.allocation),
++            rate * (diff*1.0E-9)
++        );
++
++        return (long)Math.floor(this.takeCarry + take);
++    }
++
++    // rate in units/s, and time in ns
++    public long takeAllocation(final long time, final double rate, final long maxTake) {
++        if (maxTake < 1L) {
++            return 0L;
++        }
++
++        double ret = this.takeCarry;
++        final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++        this.lastTakeUpdate = time;
++
++        // note: abs(takeCarry) <= 1.0
++        final double take = Math.min(
++            Math.min((double)maxTake - this.takeCarry, this.allocation),
++            rate * (diff*1.0E-9)
++        );
++
++        ret += take;
++        this.allocation -= take;
++
++        final long retInteger = (long)Math.floor(ret);
++        this.takeCarry = ret - (double)retInteger;
++
++        return retInteger;
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
++
++public final class Delayed26WayDistancePropagator3D {
++
++    // this map is considered "stale" unless updates are propagated.
++    protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
++
++    // this map is never stale
++    protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++    // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++    // propagating updates
++    protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++    @FunctionalInterface
++    public static interface LevelChangeCallback {
++
++        /**
++         * This can be called for intermediate updates. So do not rely on newLevel being close to or
++         * the exact level that is expected after a full propagation has occured.
++         */
++        public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++    }
++
++    protected final LevelChangeCallback changeCallback;
++
++    public Delayed26WayDistancePropagator3D() {
++        this(null);
++    }
++
++    public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++        this.changeCallback = changeCallback;
++    }
++
++    public int getLevel(final long pos) {
++        return this.levels.get(pos);
++    }
++
++    public int getLevel(final int x, final int y, final int z) {
++        return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
++    }
++
++    public void setSource(final int x, final int y, final int z, final int level) {
++        this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
++    }
++
++    public void setSource(final long coordinate, final int level) {
++        if ((level & 63) != level || level == 0) {
++            throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++        }
++
++        final byte byteLevel = (byte)level;
++        final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++        if (oldLevel == byteLevel) {
++            return; // nothing to do
++        }
++
++        // queue to update later
++        this.updatedSources.add(coordinate);
++    }
++
++    public void removeSource(final int x, final int y, final int z) {
++        this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++    }
++
++    public void removeSource(final long coordinate) {
++        if (this.sources.remove(coordinate) != 0) {
++            this.updatedSources.add(coordinate);
++        }
++    }
++
++    // queues used for BFS propagating levels
++    protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
 +    {
-+        this.objectToViewDistance.defaultReturnValue(-1);
-+        this.objectToLastCoordinate.defaultReturnValue(Long.MIN_VALUE);
-+    }
-+
-+    // we use linked for better iteration.
-+    // map of: coordinate to set of objects in coordinate
-+    protected final Long2ObjectOpenHashMap<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f);
-+    protected final PooledLinkedHashSets<E> pooledHashSets;
-+
-+    protected final ChangeCallback<E> addCallback;
-+    protected final ChangeCallback<E> removeCallback;
-+    protected final ChangeSourceCallback<E> changeSourceCallback;
-+
-+    public AreaMap() {
-+        this(new PooledLinkedHashSets<>());
-+    }
-+
-+    // let users define a "global" or "shared" pooled sets if they wish
-+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
-+        this(pooledHashSets, null, null);
-+    }
-+
-+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback) {
-+        this(pooledHashSets, addCallback, removeCallback, null);
-+    }
-+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback, final ChangeSourceCallback<E> changeSourceCallback) {
-+        this.pooledHashSets = pooledHashSets;
-+        this.addCallback = addCallback;
-+        this.removeCallback = removeCallback;
-+        this.changeSourceCallback = changeSourceCallback;
-+    }
-+
-+    @Nullable
-+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final long key) {
-+        return this.areaMap.get(key);
-+    }
-+
-+    @Nullable
-+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final ChunkPos chunkPos) {
-+        return this.areaMap.get(MCUtil.getCoordinateKey(chunkPos));
-+    }
-+
-+    @Nullable
-+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final int chunkX, final int chunkZ) {
-+        return this.areaMap.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
-+    }
-+
-+    // Long.MIN_VALUE indicates the object is not mapped
-+    public final long getLastCoordinate(final E object) {
-+        return this.objectToLastCoordinate.getOrDefault(object, Long.MIN_VALUE);
-+    }
-+
-+    // -1 indicates the object is not mapped
-+    public final int getLastViewDistance(final E object) {
-+        return this.objectToViewDistance.getOrDefault(object, -1);
-+    }
-+
-+    // returns the total number of mapped chunks
-+    public final int size() {
-+        return this.areaMap.size();
-+    }
-+
-+    public final void addOrUpdate(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+        final int oldViewDistance = this.objectToViewDistance.put(object, viewDistance);
-+        final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+        final long oldPos = this.objectToLastCoordinate.put(object, newPos);
-+
-+        if (oldViewDistance == -1) {
-+            this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
-+            this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
-+        } else {
-+            this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
-+            this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
-+        }
-+        //this.validate(object, viewDistance);
-+    }
-+
-+    public final boolean update(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+        final int oldViewDistance = this.objectToViewDistance.replace(object, viewDistance);
-+        if (oldViewDistance == -1) {
-+            return false;
-+        } else {
-+            final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+            final long oldPos = this.objectToLastCoordinate.put(object, newPos);
-+            this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
-+            this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
-+        }
-+        //this.validate(object, viewDistance);
-+        return true;
-+    }
-+
-+    // called after the distance map updates
-+    protected void updateObjectCallback(final E Object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+        if (newPosition != oldPosition && this.changeSourceCallback != null) {
-+            this.changeSourceCallback.accept(Object, oldPosition, newPosition);
++        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;
 +
-+    public final boolean add(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+        final int oldViewDistance = this.objectToViewDistance.putIfAbsent(object, viewDistance);
-+        if (oldViewDistance != -1) {
++    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;
 +        }
 +
-+        final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+        this.objectToLastCoordinate.put(object, newPos);
-+        this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
-+        this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
++        boolean ret = false;
 +
-+        //this.validate(object, viewDistance);
++        for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++            final long coordinate = iterator.nextLong();
 +
-+        return true;
-+    }
++            final byte currentLevel = this.levels.get(coordinate);
++            final byte updatedSource = this.sources.get(coordinate);
 +
-+    // called after the distance map updates
-+    protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
-+
-+    public final boolean remove(final E object) {
-+        final long position = this.objectToLastCoordinate.removeLong(object);
-+        final int viewDistance = this.objectToViewDistance.removeInt(object);
-+
-+        if (viewDistance == -1) {
-+            return false;
-+        }
-+
-+        final int currentX = MCUtil.getCoordinateX(position);
-+        final int currentZ = MCUtil.getCoordinateZ(position);
-+
-+        this.removeObject(object, currentX, currentZ, currentX, currentZ, viewDistance);
-+        this.removeObjectCallback(object, currentX, currentZ, viewDistance);
-+        //this.validate(object, -1);
-+        return true;
-+    }
-+
-+    // called after the distance map updates
-+    protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
-+
-+    protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getEmptySetFor(final E object);
-+
-+    // expensive op, only for debug
-+    protected void validate(final E object, final int viewDistance) {
-+        int entiesGot = 0;
-+        int expectedEntries = (2 * viewDistance + 1);
-+        expectedEntries *= expectedEntries;
-+        if (viewDistance < 0) {
-+            expectedEntries = 0;
-+        }
-+
-+        final long currPosition = this.objectToLastCoordinate.getLong(object);
-+
-+        final int centerX = MCUtil.getCoordinateX(currPosition);
-+        final int centerZ = MCUtil.getCoordinateZ(currPosition);
-+
-+        for (Iterator<Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>>> iterator = this.areaMap.long2ObjectEntrySet().fastIterator();
-+             iterator.hasNext();) {
-+
-+            final Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> entry = iterator.next();
-+            final long key = entry.getLongKey();
-+            final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> map = entry.getValue();
-+
-+            if (map.referenceCount == 0) {
-+                throw new IllegalStateException("Invalid map");
++            if (currentLevel == updatedSource) {
++                continue;
 +            }
++            ret = true;
 +
-+            if (map.contains(object)) {
-+                ++entiesGot;
++            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
++            }
++        }
 +
-+                final int chunkX = MCUtil.getCoordinateX(key);
-+                final int chunkZ = MCUtil.getCoordinateZ(key);
++        this.updatedSources.clear();
 +
-+                final int dist = Math.max(IntegerUtil.branchlessAbs(chunkX - centerX), IntegerUtil.branchlessAbs(chunkZ - centerZ));
++        // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++        // make the removes remove less)
++        this.propagateIncreases();
 +
-+                if (dist > viewDistance) {
-+                    throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist);
++        // 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);
++                        }
++                    }
 +                }
 +            }
 +        }
 +
-+        if (entiesGot != expectedEntries) {
-+            throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot);
-+        }
++        // propagate sources we clobbered in the process
++        this.propagateIncreases();
 +    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
 +
-+    private void addObjectTo(final E object, final int chunkX, final int chunkZ, final int currChunkX,
-+                             final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
-+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
 +
-+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> empty = this.getEmptySetFor(object);
-+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.putIfAbsent(key, empty);
++public final class Delayed8WayDistancePropagator2D {
 +
-+        if (current != null) {
-+            PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWith(current, object);
-+            if (next == current) {
-+                throw new IllegalStateException("Expected different map: got " + next.toString());
-+            }
-+            this.areaMap.put(key, next);
++    // Test
++    /*
++    protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) {
++        int got = test.getLevel(x, z);
 +
-+            current = next;
-+            // fall through to callback
-+        } else {
-+            current = empty;
-+        }
-+
-+        if (this.addCallback != null) {
-+            try {
-+                this.addCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, current);
-+            } catch (final Throwable ex) {
-+                if (ex instanceof ThreadDeath) {
-+                    throw (ThreadDeath)ex;
++        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;
++                    }
 +                }
-+                MinecraftServer.LOGGER.error("Add callback for map threw exception ", ex);
 +            }
 +        }
++
++        if (expect != got) {
++            throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
++        }
 +    }
 +
-+    private void removeObjectFrom(final E object, final int chunkX, final int chunkZ, final int currChunkX,
-+                                  final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
-+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
++    static class Ticket {
 +
-+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.get(key);
++        int x;
++        int z;
 +
-+        if (current == null) {
-+            throw new IllegalStateException("Current map may not be null for " + object + ", (" + chunkX + "," + chunkZ + ")");
-+        }
++        final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty
++                = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
 +
-+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWithout(current, object);
++    }
 +
-+        if (next == current) {
-+            throw new IllegalStateException("Current map [" + next.toString() + "] should have contained " + object + ", (" + chunkX + "," + chunkZ + ")");
-+        }
++    public static void main(final String[] args) {
++        com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
++            @Override
++            protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) {
++                return object.empty;
++            }
++        };
++        Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
 +
-+        if (next != null) {
-+            this.areaMap.put(key, next);
-+        } else {
-+            this.areaMap.remove(key);
-+        }
-+
-+        if (this.removeCallback != null) {
-+            try {
-+                this.removeCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, next);
-+            } catch (final Throwable ex) {
-+                if (ex instanceof ThreadDeath) {
-+                    throw (ThreadDeath)ex;
++        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<Ticket> list = new java.util.ArrayList<>();
++            for (int i = 0; i < 10; ++i) {
++                Ticket a = new Ticket();
++                list.add(a);
++                a.x = (i & 1) == 1 ? -i : i;
++                a.z = (i & 1) == 1 ? -i : i;
++            }
++            for (Ticket ticket : list) {
++                reference.add(ticket, ticket.x, ticket.z, originDistance);
++                test.setSource(ticket.x, ticket.z, originDistance);
++            }
++            test.propagateUpdates();
++
++            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<Ticket> list = new java.util.ArrayList<>();
++            for (int i = 0; i < 10; ++i) {
++                Ticket a = new Ticket();
++                list.add(a);
++                a.x = offX + ((i & 1) == 1 ? -i : i);
++                a.z = offZ + ((i & 1) == 1 ? -i : i);
++            }
++            for (Ticket ticket : list) {
++                reference.add(ticket, ticket.x, ticket.z, originDistance);
++                test.setSource(ticket.x, ticket.z, originDistance);
++            }
++            test.propagateUpdates();
++
++            for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++                for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++                    test(dx, dz, reference, test);
++                }
++            }
++
++            // test ticket level decrease
++
++            for (Ticket ticket : list) {
++                reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++                test.setSource(ticket.x, ticket.z, originDistance/2);
++            }
++            test.propagateUpdates();
++
++            for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++                for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++                    test(dx, dz, reference, test);
++                }
++            }
++
++            // test ticket level increase
++
++            for (Ticket ticket : list) {
++                reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++                test.setSource(ticket.x, ticket.z, originDistance*2);
++            }
++            test.propagateUpdates();
++
++            for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++                for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++                    test(dx, dz, reference, test);
++                }
++            }
++
++            // test ticket remove
++            for (int i = 0, len = list.size(); i < len; ++i) {
++                if ((i & 3) != 0) {
++                    continue;
++                }
++                Ticket ticket = list.get(i);
++                reference.remove(ticket);
++                test.removeSource(ticket.x, ticket.z);
++            }
++            test.propagateUpdates();
++
++            for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++                for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++                    test(dx, dz, reference, test);
++                }
++            }
++        }
++    }
++     */
++
++    // this map is considered "stale" unless updates are propagated.
++    protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
++
++    // this map is never stale
++    protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++    // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++    // propagating updates
++    protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++    @FunctionalInterface
++    public static interface LevelChangeCallback {
++
++        /**
++         * This can be called for intermediate updates. So do not rely on newLevel being close to or
++         * the exact level that is expected after a full propagation has occured.
++         */
++        public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
++
++    }
++
++    protected final LevelChangeCallback changeCallback;
++
++    public Delayed8WayDistancePropagator2D() {
++        this(null);
++    }
++
++    public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++        this.changeCallback = changeCallback;
++    }
++
++    public int getLevel(final long pos) {
++        return this.levels.get(pos);
++    }
++
++    public int getLevel(final int x, final int z) {
++        return this.levels.get(CoordinateUtils.getChunkKey(x, z));
++    }
++
++    public void setSource(final int x, final int z, final int level) {
++        this.setSource(CoordinateUtils.getChunkKey(x, z), level);
++    }
++
++    public void setSource(final long coordinate, final int level) {
++        if ((level & 63) != level || level == 0) {
++            throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++        }
++
++        final byte byteLevel = (byte)level;
++        final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++        if (oldLevel == byteLevel) {
++            return; // nothing to do
++        }
++
++        // queue to update later
++        this.updatedSources.add(coordinate);
++    }
++
++    public void removeSource(final int x, final int z) {
++        this.removeSource(CoordinateUtils.getChunkKey(x, z));
++    }
++
++    public void removeSource(final long coordinate) {
++        if (this.sources.remove(coordinate) != 0) {
++            this.updatedSources.add(coordinate);
++        }
++    }
++
++    // queues used for BFS propagating levels
++    protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
++    {
++        for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++            this.levelIncreaseWorkQueues[i] = new WorkQueue();
++        }
++    }
++    protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
++    {
++        for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++            this.levelRemoveWorkQueues[i] = new WorkQueue();
++        }
++    }
++    protected long levelIncreaseWorkQueueBitset;
++    protected long levelRemoveWorkQueueBitset;
++
++    protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++        final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++        queue.queuedCoordinates.enqueue(coordinate);
++        queue.queuedLevels.enqueue(level);
++
++        this.levelIncreaseWorkQueueBitset |= (1L << level);
++    }
++
++    protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++        final WorkQueue queue = this.levelIncreaseWorkQueues[index];
++        queue.queuedCoordinates.enqueue(coordinate);
++        queue.queuedLevels.enqueue(level);
++
++        this.levelIncreaseWorkQueueBitset |= (1L << index);
++    }
++
++    protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++        final WorkQueue queue = this.levelRemoveWorkQueues[level];
++        queue.queuedCoordinates.enqueue(coordinate);
++        queue.queuedLevels.enqueue(level);
++
++        this.levelRemoveWorkQueueBitset |= (1L << level);
++    }
++
++    public boolean propagateUpdates() {
++        if (this.updatedSources.isEmpty()) {
++            return false;
++        }
++
++        boolean ret = false;
++
++        for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++            final long coordinate = iterator.nextLong();
++
++            final byte currentLevel = this.levels.get(coordinate);
++            final byte updatedSource = this.sources.get(coordinate);
++
++            if (currentLevel == updatedSource) {
++                continue;
++            }
++            ret = true;
++
++            if (updatedSource > currentLevel) {
++                // level increase
++                this.addToIncreaseWorkQueue(coordinate, updatedSource);
++            } else {
++                // level decrease
++                this.addToRemoveWorkQueue(coordinate, currentLevel);
++                // if the current coordinate is a source, then the decrease propagation will detect that and queue
++                // the source propagation
++            }
++        }
++
++        this.updatedSources.clear();
++
++        // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++        // make the removes remove less)
++        this.propagateIncreases();
++
++        // now we propagate the decreases (which will then re-propagate clobbered sources)
++        this.propagateDecreases();
++
++        return ret;
++    }
++
++    protected void propagateIncreases() {
++        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++             this.levelIncreaseWorkQueueBitset != 0L;
++             this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++            final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++            while (!queue.queuedLevels.isEmpty()) {
++                final long coordinate = queue.queuedCoordinates.removeFirstLong();
++                byte level = queue.queuedLevels.removeFirstByte();
++
++                final boolean neighbourCheck = level < 0;
++
++                final byte currentLevel;
++                if (neighbourCheck) {
++                    level = (byte)-level;
++                    currentLevel = this.levels.get(coordinate);
++                } else {
++                    currentLevel = this.levels.putIfGreater(coordinate, level);
++                }
++
++                if (neighbourCheck) {
++                    // used when propagating from decrease to indicate that this level needs to check its neighbours
++                    // this means the level at coordinate could be equal, but would still need neighbours checked
++
++                    if (currentLevel != level) {
++                        // something caused the level to change, which means something propagated to it (which means
++                        // us propagating here is redundant), or something removed the level (which means we
++                        // cannot propagate further)
++                        continue;
++                    }
++                } else if (currentLevel >= level) {
++                    // something higher/equal propagated
++                    continue;
++                }
++                if (this.changeCallback != null) {
++                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++                }
++
++                if (level == 1) {
++                    // can't propagate 0 to neighbours
++                    continue;
++                }
++
++                // propagate to neighbours
++                final byte neighbourLevel = (byte)(level - 1);
++                final int x = (int)coordinate;
++                final int z = (int)(coordinate >>> 32);
++
++                for (int dx = -1; dx <= 1; ++dx) {
++                    for (int dz = -1; dz <= 1; ++dz) {
++                        if ((dx | dz) == 0) {
++                            // already propagated to coordinate
++                            continue;
++                        }
++
++                        // sure we can check the neighbour level in the map right now and avoid a propagation,
++                        // but then we would still have to recheck it when popping the value off of the queue!
++                        // so just avoid the double lookup
++                        final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++                        this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++                    }
 +                }
-+                MinecraftServer.LOGGER.error("Remove callback for map threw exception ", ex);
 +            }
 +        }
 +    }
 +
-+    private void addObject(final E object, final int chunkX, final int chunkZ, final int prevChunkX, final int prevChunkZ, final int viewDistance) {
-+        final int maxX = chunkX + viewDistance;
-+        final int maxZ = chunkZ + viewDistance;
-+        final int minX = chunkX - viewDistance;
-+        final int minZ = chunkZ - viewDistance;
-+        for (int x = minX; x <= maxX; ++x) {
-+            for (int z = minZ; z <= maxZ; ++z) {
-+                this.addObjectTo(object, x, z, chunkX, chunkZ, prevChunkX, prevChunkZ);
++    protected void propagateDecreases() {
++        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++             this.levelRemoveWorkQueueBitset != 0L;
++             this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++            final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++            while (!queue.queuedLevels.isEmpty()) {
++                final long coordinate = queue.queuedCoordinates.removeFirstLong();
++                final byte level = queue.queuedLevels.removeFirstByte();
++
++                final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++                if (currentLevel == 0) {
++                    // something else removed
++                    continue;
++                }
++
++                if (currentLevel > level) {
++                    // something higher propagated here or we hit the propagation of another source
++                    // in the second case we need to re-propagate because we could have just clobbered another source's
++                    // propagation
++                    this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++                    continue;
++                }
++
++                if (this.changeCallback != null) {
++                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++                }
++
++                final byte source = this.sources.get(coordinate);
++                if (source != 0) {
++                    // must re-propagate source later
++                    this.addToIncreaseWorkQueue(coordinate, source);
++                }
++
++                if (level == 0) {
++                    // can't propagate -1 to neighbours
++                    // we have to check neighbours for removing 1 just in case the neighbour is 2
++                    continue;
++                }
++
++                // propagate to neighbours
++                final byte neighbourLevel = (byte)(level - 1);
++                final int x = (int)coordinate;
++                final int z = (int)(coordinate >>> 32);
++
++                for (int dx = -1; dx <= 1; ++dx) {
++                    for (int dz = -1; dz <= 1; ++dz) {
++                        if ((dx | dz) == 0) {
++                            // already propagated to coordinate
++                            continue;
++                        }
++
++                        // sure we can check the neighbour level in the map right now and avoid a propagation,
++                        // but then we would still have to recheck it when popping the value off of the queue!
++                        // so just avoid the double lookup
++                        final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++                        this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++                    }
++                }
++            }
++        }
++
++        // propagate sources we clobbered in the process
++        this.propagateIncreases();
++    }
++
++    protected static final class LevelMap extends Long2ByteOpenHashMap {
++        public LevelMap() {
++            super();
++        }
++
++        public LevelMap(final int expected, final float loadFactor) {
++            super(expected, loadFactor);
++        }
++
++        // copied from superclass
++        private int find(final long k) {
++            if (k == 0L) {
++                return this.containsNullKey ? this.n : -(this.n + 1);
++            } else {
++                final long[] key = this.key;
++                long curr;
++                int pos;
++                if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
++                    return -(pos + 1);
++                } else if (k == curr) {
++                    return pos;
++                } else {
++                    while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
++                        if (k == curr) {
++                            return pos;
++                        }
++                    }
++
++                    return -(pos + 1);
++                }
++            }
++        }
++
++        // copied from superclass
++        private void insert(final int pos, final long k, final byte v) {
++            if (pos == this.n) {
++                this.containsNullKey = true;
++            }
++
++            this.key[pos] = k;
++            this.value[pos] = v;
++            if (this.size++ >= this.maxFill) {
++                this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++            }
++        }
++
++        // copied from superclass
++        public byte putIfGreater(final long key, final byte value) {
++            final int pos = this.find(key);
++            if (pos < 0) {
++                if (this.defRetValue < value) {
++                    this.insert(-pos - 1, key, value);
++                }
++                return this.defRetValue;
++            } else {
++                final byte curr = this.value[pos];
++                if (value > curr) {
++                    this.value[pos] = value;
++                    return curr;
++                }
++                return curr;
++            }
++        }
++
++        // copied from superclass
++        private void removeEntry(final int pos) {
++            --this.size;
++            this.shiftKeys(pos);
++            if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++                this.rehash(this.n / 2);
++            }
++        }
++
++        // copied from superclass
++        private void removeNullEntry() {
++            this.containsNullKey = false;
++            --this.size;
++            if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++                this.rehash(this.n / 2);
++            }
++        }
++
++        // copied from superclass
++        public byte removeIfGreaterOrEqual(final long key, final byte value) {
++            if (key == 0L) {
++                if (!this.containsNullKey) {
++                    return this.defRetValue;
++                }
++                final byte current = this.value[this.n];
++                if (value >= current) {
++                    this.removeNullEntry();
++                    return current;
++                }
++                return current;
++            } else {
++                long[] keys = this.key;
++                byte[] values = this.value;
++                long curr;
++                int pos;
++                if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
++                    return this.defRetValue;
++                } else if (key == curr) {
++                    final byte current = values[pos];
++                    if (value >= current) {
++                        this.removeEntry(pos);
++                        return current;
++                    }
++                    return current;
++                } else {
++                    while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
++                        if (key == curr) {
++                            final byte current = values[pos];
++                            if (value >= current) {
++                                this.removeEntry(pos);
++                                return current;
++                            }
++                            return current;
++                        }
++                    }
++
++                    return this.defRetValue;
++                }
 +            }
 +        }
 +    }
 +
-+    private void removeObject(final E object, final int chunkX, final int chunkZ, final int currentChunkX, final int currentChunkZ, final int viewDistance) {
-+        final int maxX = chunkX + viewDistance;
-+        final int maxZ = chunkZ + viewDistance;
-+        final int minX = chunkX - viewDistance;
-+        final int minZ = chunkZ - viewDistance;
-+        for (int x = minX; x <= maxX; ++x) {
-+            for (int z = minZ; z <= maxZ; ++z) {
-+                this.removeObjectFrom(object, x, z, currentChunkX, currentChunkZ, chunkX, chunkZ);
++    protected static final class WorkQueue {
++
++        public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
++        public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
++
++    }
++
++    protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
++
++        /**
++         * Assumes non-empty. If empty, undefined behaviour.
++         */
++        public long removeFirstLong() {
++            // copied from superclass
++            long t = this.array[this.start];
++            if (++this.start == this.length) {
++                this.start = 0;
++            }
++
++            return t;
++        }
++    }
++
++    protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
++
++        /**
++         * Assumes non-empty. If empty, undefined behaviour.
++         */
++        public byte removeFirstByte() {
++            // copied from superclass
++            byte t = this.array[this.start];
++            if (++this.start == this.length) {
++                this.start = 0;
++            }
++
++            return t;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.level.ChunkPos;
++
++public final class NearbyPlayers {
++
++    public static enum NearbyMapType {
++        GENERAL,
++        GENERAL_SMALL,
++        GENERAL_REALLY_SMALL,
++        TICK_VIEW_DISTANCE,
++        VIEW_DISTANCE,
++        SPAWN_RANGE, // Moonrise - chunk tick iteration
++    }
++
++    private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
++    public static final int TOTAL_MAP_TYPES = MAP_TYPES.length;
++
++    private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1;
++    private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
++    private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
++
++    public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
++    public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
++    public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
++
++    private final ServerLevel world;
++    private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
++    private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
++
++    public NearbyPlayers(final ServerLevel world) {
++        this.world = world;
++    }
++
++    public void addPlayer(final ServerPlayer player) {
++        final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
++        if (this.players.putIfAbsent(player, newTrackers) != null) {
++            throw new IllegalStateException("Already have player " + player);
++        }
++
++        final ChunkPos chunk = player.chunkPosition();
++
++        for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
++            // use 0 for default, will be updated by tickPlayer
++            (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0);
++        }
++
++        // update view distances
++        this.tickPlayer(player);
++    }
++
++    public void removePlayer(final ServerPlayer player) {
++        final TrackedPlayer[] players = this.players.remove(player);
++        if (players == null) {
++            return; // May be called during teleportation before the player is actually placed
++        }
++
++        for (final TrackedPlayer tracker : players) {
++            tracker.remove();
++        }
++    }
++
++    public void tickPlayer(final ServerPlayer player) {
++        final TrackedPlayer[] players = this.players.get(player);
++        if (players == null) {
++            throw new IllegalStateException("Don't have player " + player);
++        }
++
++        final ChunkPos chunk = player.chunkPosition();
++
++        players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
++        players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
++        players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
++        players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
++        players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
++        players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
++    }
++
++    public TrackedChunk getChunk(final ChunkPos pos) {
++        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++    }
++
++    public TrackedChunk getChunk(final BlockPos pos) {
++        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++    }
++
++    public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
++        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++
++        return chunk == null ? null : chunk.players[type.ordinal()];
++    }
++
++    public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
++        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++
++        return chunk == null ? null : chunk.players[type.ordinal()];
++    }
++
++    public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
++        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++        return chunk == null ? null : chunk.players[type.ordinal()];
++    }
++
++    public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
++        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
++
++        return chunk == null ? null : chunk.players[type.ordinal()];
++    }
++
++    public static final class TrackedChunk {
++
++        private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
++
++        private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
++        private int nonEmptyLists;
++        private long updateCount;
++
++        public boolean isEmpty() {
++            return this.nonEmptyLists == 0;
++        }
++
++        public long getUpdateCount() {
++            return this.updateCount;
++        }
++
++        public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
++            return this.players[type.ordinal()];
++        }
++
++        public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
++            ++this.updateCount;
++
++            final int idx = type.ordinal();
++            final ReferenceList<ServerPlayer> list = this.players[idx];
++            if (list == null) {
++                ++this.nonEmptyLists;
++                (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)).add(player);
++                return;
++            }
++
++            if (!list.add(player)) {
++                throw new IllegalStateException("Already contains player " + player);
 +            }
 +        }
++
++        public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
++            ++this.updateCount;
++
++            final int idx = type.ordinal();
++            final ReferenceList<ServerPlayer> list = this.players[idx];
++            if (list == null) {
++                throw new IllegalStateException("Does not contain player " + player);
++            }
++
++            if (!list.remove(player)) {
++                throw new IllegalStateException("Does not contain player " + player);
++            }
++
++            if (list.size() == 0) {
++                this.players[idx] = null;
++                --this.nonEmptyLists;
++            }
++        }
++    }
++
++    private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
++
++        private final NearbyMapType type;
++
++        public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
++            super(player);
++            this.type = type;
++        }
++
++        @Override
++        protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++            NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
++                return new TrackedChunk();
++            }).addPlayer(parameter, this.type);
++        }
++
++        @Override
++        protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++            final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
++            if (chunk == null) {
++                throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
++            }
++
++            chunk.removePlayer(parameter, this.type);
++
++            if (chunk.isEmpty()) {
++                NearbyPlayers.this.byChunk.remove(chunkKey);
++            }
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.IntPairUtil;
++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceSet;
++
++public final class PositionCountingAreaMap<T> {
++
++    private final Reference2ReferenceOpenHashMap<T, PositionCounter> counters = new Reference2ReferenceOpenHashMap<>();
++    private final Long2IntOpenHashMap positions = new Long2IntOpenHashMap();
++
++    public ReferenceSet<T> getObjects() {
++        return this.counters.keySet();
++    }
++
++    public int getTotalPositions() {
++        return this.positions.size();
++    }
++
++    public boolean hasObjectsNear(final int toX, final int toZ) {
++        return this.positions.containsKey(IntPairUtil.key(toX, toZ));
++    }
++
++    public int getObjectsNear(final int toX, final int toZ) {
++        return this.positions.get(IntPairUtil.key(toX, toZ));
++    }
++
++    public boolean add(final T parameter, final int toX, final int toZ, final int distance) {
++        final PositionCounter existing = this.counters.get(parameter);
++        if (existing != null) {
++            return false;
++        }
++
++        final PositionCounter counter = new PositionCounter(parameter);
++
++        this.counters.put(parameter, counter);
++
++        return counter.add(toX, toZ, distance);
++    }
++
++    public boolean addOrUpdate(final T parameter, final int toX, final int toZ, final int distance) {
++        final PositionCounter existing = this.counters.get(parameter);
++        if (existing != null) {
++            return existing.update(toX, toZ, distance);
++        }
++
++        final PositionCounter counter = new PositionCounter(parameter);
++
++        this.counters.put(parameter, counter);
++
++        return counter.add(toX, toZ, distance);
++    }
++
++    public boolean remove(final T parameter) {
++        final PositionCounter counter = this.counters.remove(parameter);
++        if (counter == null) {
++            return false;
++        }
++
++        counter.remove();
++
++        return true;
++    }
++
++    public boolean update(final T parameter, final int toX, final int toZ, final int distance) {
++        final PositionCounter counter = this.counters.get(parameter);
++        if (counter == null) {
++            return false;
++        }
++
++        return counter.update(toX, toZ, distance);
++    }
++
++    private final class PositionCounter extends SingleUserAreaMap<T> {
++
++        public PositionCounter(final T parameter) {
++            super(parameter);
++        }
++
++        @Override
++        protected void addCallback(final T parameter, final int toX, final int toZ) {
++            PositionCountingAreaMap.this.positions.addTo(IntPairUtil.key(toX, toZ), 1);
++        }
++
++        @Override
++        protected void removeCallback(final T parameter, final int toX, final int toZ) {
++            final long key = IntPairUtil.key(toX, toZ);
++            if (PositionCountingAreaMap.this.positions.addTo(key, -1) == 1) {
++                PositionCountingAreaMap.this.positions.remove(key);
++            }
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
++
++public abstract class SingleUserAreaMap<T> {
++
++    public static final int NOT_SET = Integer.MIN_VALUE;
++
++    private final T parameter;
++    private int lastChunkX = NOT_SET;
++    private int lastChunkZ = NOT_SET;
++    private int distance = NOT_SET;
++
++    public SingleUserAreaMap(final T parameter) {
++        this.parameter = parameter;
++    }
++
++    public final T getParameter() {
++        return this.parameter;
++    }
++
++    public final int getLastChunkX() {
++        return this.lastChunkX;
++    }
++
++    public final int getLastChunkZ() {
++        return this.lastChunkZ;
++    }
++
++    public final int getLastDistance() {
++        return this.distance;
 +    }
 +
 +    /* math sign function except 0 returns 1 */
@@ -851,11 +2774,65 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return 1 | (val >> (Integer.SIZE - 1));
 +    }
 +
-+    private void updateObject(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+        final int toX = MCUtil.getCoordinateX(newPosition);
-+        final int toZ = MCUtil.getCoordinateZ(newPosition);
-+        final int fromX = MCUtil.getCoordinateX(oldPosition);
-+        final int fromZ = MCUtil.getCoordinateZ(oldPosition);
++    protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
++
++    protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
++
++    private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++        final int maxX = chunkX + distance;
++        final int maxZ = chunkZ + distance;
++
++        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++                this.addCallback(parameter, cx, cz);
++            }
++        }
++    }
++
++    private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++        final int maxX = chunkX + distance;
++        final int maxZ = chunkZ + distance;
++
++        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++                this.removeCallback(parameter, cx, cz);
++            }
++        }
++    }
++
++    public final boolean add(final int chunkX, final int chunkZ, final int distance) {
++        if (distance < 0) {
++            throw new IllegalArgumentException(Integer.toString(distance));
++        }
++        if (this.lastChunkX != NOT_SET) {
++            return false;
++        }
++        this.lastChunkX = chunkX;
++        this.lastChunkZ = chunkZ;
++        this.distance = distance;
++
++        this.addToNew(this.parameter, chunkX, chunkZ, distance);
++
++        return true;
++    }
++
++    public final boolean update(final int toX, final int toZ, final int newViewDistance) {
++        if (newViewDistance < 0) {
++            throw new IllegalArgumentException(Integer.toString(newViewDistance));
++        }
++        final int fromX = this.lastChunkX;
++        final int fromZ = this.lastChunkZ;
++        final int oldViewDistance = this.distance;
++        if (fromX == NOT_SET) {
++            return false;
++        }
++
++        this.lastChunkX = toX;
++        this.lastChunkZ = toZ;
++        this.distance = newViewDistance;
++
++        final T parameter = this.parameter;
++
 +
 +        final int dx = toX - fromX;
 +        final int dz = toZ - fromZ;
@@ -864,10 +2841,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
 +
 +        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+            // teleported?
-+            this.removeObject(object, fromX, fromZ, fromX, fromZ, oldViewDistance);
-+            this.addObject(object, toX, toZ, fromX, fromZ, newViewDistance);
-+            return;
++            // teleported
++            this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
++            this.addToNew(parameter, toX, toZ, newViewDistance);
++            return true;
 +        }
 +
 +        if (oldViewDistance != newViewDistance) {
@@ -882,7 +2859,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +                    // only remove if we're outside the new view distance...
 +                    if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
-+                        this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
++                        this.removeCallback(parameter, currX, currZ);
 +                    }
 +                }
 +            }
@@ -898,12 +2875,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +                    // only add if we're outside the old view distance...
 +                    if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
-+                        this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
++                        this.addCallback(parameter, currX, currZ);
 +                    }
 +                }
 +            }
 +
-+            return;
++            return true;
 +        }
 +
 +        // x axis is width
@@ -939,7 +2916,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (int currX = minX; currX != maxX; currX += right) {
 +                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
++                    this.addCallback(parameter, currX, currZ);
 +                }
 +            }
 +        }
@@ -954,7 +2931,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (int currX = minX; currX != maxX; currX += right) {
 +                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
++                    this.addCallback(parameter, currX, currZ);
 +                }
 +            }
 +        }
@@ -969,7 +2946,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (int currX = minX; currX != maxX; currX += right) {
 +                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
++                    this.removeCallback(parameter, currX, currZ);
 +                }
 +            }
 +        }
@@ -984,672 +2961,39 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (int currX = minX; currX != maxX; currX += right) {
 +                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
++                    this.removeCallback(parameter, currX, currZ);
 +                }
 +            }
 +        }
++
++        return true;
 +    }
 +
-+    @FunctionalInterface
-+    public static interface ChangeCallback<E> {
++    public final boolean remove() {
++        final int chunkX = this.lastChunkX;
++        final int chunkZ = this.lastChunkZ;
++        final int distance = this.distance;
++        if (chunkX == NOT_SET) {
++            return false;
++        }
 +
-+        // if there is no previous position, then prevPos = Integer.MIN_VALUE
-+        void accept(final E object, final int rangeX, final int rangeZ, final int currPosX, final int currPosZ, final int prevPosX, final int prevPosZ,
-+                    final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> newState);
++        this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
 +
-+    }
++        this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
 +
-+    @FunctionalInterface
-+    public static interface ChangeSourceCallback<E> {
-+        void accept(final E object, final long prevPos, final long newPos);
++        return true;
 +    }
 +}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
 @@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.misc;
-+
-+import io.papermc.paper.util.IntegerUtil;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.world.level.ChunkPos;
-+
-+/** @author Spottedleaf */
-+public abstract class DistanceTrackingAreaMap<E> extends AreaMap<E> {
-+
-+    // use this map only if you need distance tracking, the tracking here is obviously going to hit harder.
-+
-+    protected final Long2IntOpenHashMap chunkToNearestDistance = new Long2IntOpenHashMap(1024, 0.7f);
-+    {
-+        this.chunkToNearestDistance.defaultReturnValue(-1);
-+    }
-+
-+    protected final DistanceChangeCallback<E> distanceChangeCallback;
-+
-+    public DistanceTrackingAreaMap() {
-+        this(new PooledLinkedHashSets<>());
-+    }
-+
-+    // let users define a "global" or "shared" pooled sets if they wish
-+    public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
-+        this(pooledHashSets, null, null, null);
-+    }
-+
-+    public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback,
-+                                   final DistanceChangeCallback<E> distanceChangeCallback) {
-+        super(pooledHashSets, addCallback, removeCallback);
-+        this.distanceChangeCallback = distanceChangeCallback;
-+    }
-+
-+    // ret -1 if there is nothing mapped
-+    public final int getNearestObjectDistance(final long key) {
-+        return this.chunkToNearestDistance.get(key);
-+    }
-+
-+    // ret -1 if there is nothing mapped
-+    public final int getNearestObjectDistance(final ChunkPos chunkPos) {
-+        return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkPos));
-+    }
-+
-+    // ret -1 if there is nothing mapped
-+    public final int getNearestObjectDistance(final int chunkX, final int chunkZ) {
-+        return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
-+    }
-+
-+    protected final void recalculateDistance(final int chunkX, final int chunkZ) {
-+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+        final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state = this.areaMap.get(key);
-+        if (state == null) {
-+            final int oldDistance = this.chunkToNearestDistance.remove(key);
-+            // nothing here.
-+            if (oldDistance == -1) {
-+                // nothing was here previously
-+                return;
-+            }
-+            if (this.distanceChangeCallback != null) {
-+                this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, -1, null);
-+            }
-+            return;
-+        }
-+
-+        int newDistance = Integer.MAX_VALUE;
-+
-+        final Object[] rawData = state.getBackingSet();
-+        for (int i = 0, len = rawData.length; i < len; ++i) {
-+            final Object raw = rawData[i];
-+
-+            if (raw == null) {
-+                continue;
-+            }
-+
-+            final E object = (E)raw;
-+            final long location = this.objectToLastCoordinate.getLong(object);
-+
-+            final int distance = Math.max(IntegerUtil.branchlessAbs(chunkX - MCUtil.getCoordinateX(location)), IntegerUtil.branchlessAbs(chunkZ - MCUtil.getCoordinateZ(location)));
-+
-+            if (distance < newDistance) {
-+                newDistance = distance;
-+            }
-+        }
-+
-+        final int oldDistance = this.chunkToNearestDistance.put(key, newDistance);
-+
-+        if (oldDistance != newDistance) {
-+            if (this.distanceChangeCallback != null) {
-+                this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, newDistance, state);
-+            }
-+        }
-+    }
-+
-+    @Override
-+    protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+        final int maxX = chunkX + viewDistance;
-+        final int maxZ = chunkZ + viewDistance;
-+        final int minX = chunkX - viewDistance;
-+        final int minZ = chunkZ - viewDistance;
-+        for (int x = minX; x <= maxX; ++x) {
-+            for (int z = minZ; z <= maxZ; ++z) {
-+                this.recalculateDistance(x, z);
-+            }
-+        }
-+    }
-+
-+    @Override
-+    protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+        final int maxX = chunkX + viewDistance;
-+        final int maxZ = chunkZ + viewDistance;
-+        final int minX = chunkX - viewDistance;
-+        final int minZ = chunkZ - viewDistance;
-+        for (int x = minX; x <= maxX; ++x) {
-+            for (int z = minZ; z <= maxZ; ++z) {
-+                this.recalculateDistance(x, z);
-+            }
-+        }
-+    }
-+
-+    @Override
-+    protected void updateObjectCallback(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+        if (oldPosition == newPosition && newViewDistance == oldViewDistance) {
-+            return;
-+        }
-+
-+        final int toX = MCUtil.getCoordinateX(newPosition);
-+        final int toZ = MCUtil.getCoordinateZ(newPosition);
-+        final int fromX = MCUtil.getCoordinateX(oldPosition);
-+        final int fromZ = MCUtil.getCoordinateZ(oldPosition);
-+
-+        final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
-+
-+        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+            // teleported?
-+            this.removeObjectCallback(object, fromX, fromZ, oldViewDistance);
-+            this.addObjectCallback(object, toX, toZ, newViewDistance);
-+            return;
-+        }
-+
-+        final int minX = Math.min(fromX - oldViewDistance, toX - newViewDistance);
-+        final int maxX = Math.max(fromX + oldViewDistance, toX + newViewDistance);
-+        final int minZ = Math.min(fromZ - oldViewDistance, toZ - newViewDistance);
-+        final int maxZ = Math.max(fromZ + oldViewDistance, toZ + newViewDistance);
-+
-+        for (int x = minX; x <= maxX; ++x) {
-+            for (int z = minZ; z <= maxZ; ++z) {
-+                final int distXOld = IntegerUtil.branchlessAbs(x - fromX);
-+                final int distZOld = IntegerUtil.branchlessAbs(z - fromZ);
-+
-+                if (Math.max(distXOld, distZOld) <= oldViewDistance) {
-+                    this.recalculateDistance(x, z);
-+                    continue;
-+                }
-+
-+                final int distXNew = IntegerUtil.branchlessAbs(x - toX);
-+                final int distZNew = IntegerUtil.branchlessAbs(z - toZ);
-+
-+                if (Math.max(distXNew, distZNew) <= newViewDistance) {
-+                    this.recalculateDistance(x, z);
-+                    continue;
-+                }
-+            }
-+        }
-+    }
-+
-+    @FunctionalInterface
-+    public static interface DistanceChangeCallback<E> {
-+
-+        void accept(final int posX, final int posZ, final int oldNearestDistance, final int newNearestDistance,
-+                    final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state);
-+
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.misc;
-+
-+import net.minecraft.server.level.ServerPlayer;
-+
-+/**
-+ * @author Spottedleaf
-+ */
-+public final class PlayerAreaMap extends AreaMap<ServerPlayer> {
-+
-+    public PlayerAreaMap() {
-+        super();
-+    }
-+
-+    public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) {
-+        super(pooledHashSets);
-+    }
-+
-+    public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+                         final ChangeCallback<ServerPlayer> removeCallback) {
-+        this(pooledHashSets, addCallback, removeCallback, null);
-+    }
-+
-+    public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+                         final ChangeCallback<ServerPlayer> removeCallback, final ChangeSourceCallback<ServerPlayer> changeSourceCallback) {
-+        super(pooledHashSets, addCallback, removeCallback, changeSourceCallback);
-+    }
-+
-+    @Override
-+    protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) {
-+        return player.cachedSingleHashSet;
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.misc;
-+
-+import net.minecraft.server.level.ServerPlayer;
-+
-+public class PlayerDistanceTrackingAreaMap extends DistanceTrackingAreaMap<ServerPlayer> {
-+
-+    public PlayerDistanceTrackingAreaMap() {
-+        super();
-+    }
-+
-+    public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) {
-+        super(pooledHashSets);
-+    }
-+
-+    public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+                                         final ChangeCallback<ServerPlayer> removeCallback, final DistanceChangeCallback<ServerPlayer> distanceChangeCallback) {
-+        super(pooledHashSets, addCallback, removeCallback, distanceChangeCallback);
-+    }
-+
-+    @Override
-+    protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) {
-+        return player.cachedSingleHashSet;
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.misc;
-+
-+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-+import java.lang.ref.WeakReference;
-+
-+/** @author Spottedleaf */
-+public class PooledLinkedHashSets<E> {
-+
-+    /* Tested via https://gist.github.com/Spottedleaf/a93bb7a8993d6ce142d3efc5932bf573 */
-+
-+    // we really want to avoid that equals() check as much as possible...
-+    protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(128, 0.25f);
-+
-+    protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> current) {
-+        if (current.referenceCount == 0) {
-+            throw new IllegalStateException("Cannot decrement reference count for " + current);
-+        }
-+        if (current.referenceCount == -1 || --current.referenceCount > 0) {
-+            return;
-+        }
-+
-+        this.mapPool.remove(current);
-+        return;
-+    }
-+
-+    public PooledObjectLinkedOpenHashSet<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
-+        final PooledObjectLinkedOpenHashSet<E> cached = current.getAddCache(object);
-+
-+        if (cached != null) {
-+            decrementReferenceCount(current);
-+
-+            if (cached.referenceCount == 0) {
-+                // bring the map back from the dead
-+                PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
-+                if (contending != null) {
-+                    // a map already exists with the elements we want
-+                    if (contending.referenceCount != -1) {
-+                        ++contending.referenceCount;
-+                    }
-+                    current.updateAddCache(object, contending);
-+                    return contending;
-+                }
-+
-+                cached.referenceCount = 1;
-+            } else if (cached.referenceCount != -1) {
-+                ++cached.referenceCount;
-+            }
-+
-+            return cached;
-+        }
-+
-+        if (!current.add(object)) {
-+            return current;
-+        }
-+
-+        // we use get/put since we use a different key on put
-+        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
-+
-+        if (ret == null) {
-+            ret = new PooledObjectLinkedOpenHashSet<>(current);
-+            current.remove(object);
-+            this.mapPool.put(ret, ret);
-+            ret.referenceCount = 1;
-+        } else {
-+            if (ret.referenceCount != -1) {
-+                ++ret.referenceCount;
-+            }
-+            current.remove(object);
-+        }
-+
-+        current.updateAddCache(object, ret);
-+
-+        decrementReferenceCount(current);
-+        return ret;
-+    }
-+
-+    // rets null if current.size() == 1
-+    public PooledObjectLinkedOpenHashSet<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
-+        if (current.set.size() == 1) {
-+            decrementReferenceCount(current);
-+            return null;
-+        }
-+
-+        final PooledObjectLinkedOpenHashSet<E> cached = current.getRemoveCache(object);
-+
-+        if (cached != null) {
-+            decrementReferenceCount(current);
-+
-+            if (cached.referenceCount == 0) {
-+                // bring the map back from the dead
-+                PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
-+                if (contending != null) {
-+                    // a map already exists with the elements we want
-+                    if (contending.referenceCount != -1) {
-+                        ++contending.referenceCount;
-+                    }
-+                    current.updateRemoveCache(object, contending);
-+                    return contending;
-+                }
-+
-+                cached.referenceCount = 1;
-+            } else if (cached.referenceCount != -1) {
-+                ++cached.referenceCount;
-+            }
-+
-+            return cached;
-+        }
-+
-+        if (!current.remove(object)) {
-+            return current;
-+        }
-+
-+        // we use get/put since we use a different key on put
-+        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
-+
-+        if (ret == null) {
-+            ret = new PooledObjectLinkedOpenHashSet<>(current);
-+            current.add(object);
-+            this.mapPool.put(ret, ret);
-+            ret.referenceCount = 1;
-+        } else {
-+            if (ret.referenceCount != -1) {
-+                ++ret.referenceCount;
-+            }
-+            current.add(object);
-+        }
-+
-+        current.updateRemoveCache(object, ret);
-+
-+        decrementReferenceCount(current);
-+        return ret;
-+    }
-+
-+    static final class RawSetObjectLinkedOpenHashSet<E> extends ObjectOpenHashSet<E> {
-+
-+        public RawSetObjectLinkedOpenHashSet() {
-+            super();
-+        }
-+
-+        public RawSetObjectLinkedOpenHashSet(final int capacity) {
-+            super(capacity);
-+        }
-+
-+        public RawSetObjectLinkedOpenHashSet(final int capacity, final float loadFactor) {
-+            super(capacity, loadFactor);
-+        }
-+
-+        @Override
-+        public RawSetObjectLinkedOpenHashSet<E> clone() {
-+            return (RawSetObjectLinkedOpenHashSet<E>)super.clone();
-+        }
-+
-+        public E[] getRawSet() {
-+            return this.key;
-+        }
-+    }
-+
-+    public static final class PooledObjectLinkedOpenHashSet<E> {
-+
-+        private static final WeakReference NULL_REFERENCE = new WeakReference<>(null);
-+
-+        final RawSetObjectLinkedOpenHashSet<E> set;
-+        int referenceCount; // -1 if special
-+        int hash; // optimize hashcode
-+
-+        // add cache
-+        WeakReference<E> lastAddObject = NULL_REFERENCE;
-+        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE;
-+
-+        // remove cache
-+        WeakReference<E> lastRemoveObject = NULL_REFERENCE;
-+        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastRemoveMap = NULL_REFERENCE;
-+
-+        public PooledObjectLinkedOpenHashSet(final PooledLinkedHashSets<E> pooledSets) {
-+            this.set = new RawSetObjectLinkedOpenHashSet<>(2, 0.8f);
-+        }
-+
-+        public PooledObjectLinkedOpenHashSet(final E single) {
-+            this((PooledLinkedHashSets<E>)null);
-+            this.referenceCount = -1;
-+            this.add(single);
-+        }
-+
-+        public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet<E> other) {
-+            this.set = other.set.clone();
-+            this.hash = other.hash;
-+        }
-+
-+        // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
-+        // generated by https://github.com/skeeto/hash-prospector
-+        private static int hash0(int x) {
-+            x *= 0x36935555;
-+            x ^= x >>> 16;
-+            return x;
-+        }
-+
-+        PooledObjectLinkedOpenHashSet<E> getAddCache(final E element) {
-+            final E currentAdd = this.lastAddObject.get();
-+
-+            if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
-+                return null;
-+            }
-+
-+            return this.lastAddMap.get();
-+        }
-+
-+        PooledObjectLinkedOpenHashSet<E> getRemoveCache(final E element) {
-+            final E currentRemove = this.lastRemoveObject.get();
-+
-+            if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
-+                return null;
-+            }
-+
-+            return this.lastRemoveMap.get();
-+        }
-+
-+        void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
-+            this.lastAddObject = new WeakReference<>(element);
-+            this.lastAddMap = new WeakReference<>(map);
-+        }
-+
-+        void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
-+            this.lastRemoveObject = new WeakReference<>(element);
-+            this.lastRemoveMap = new WeakReference<>(map);
-+        }
-+
-+        boolean add(final E element) {
-+            boolean added =  this.set.add(element);
-+
-+            if (added) {
-+                this.hash += hash0(element.hashCode());
-+            }
-+
-+            return added;
-+        }
-+
-+        boolean remove(Object element) {
-+            boolean removed = this.set.remove(element);
-+
-+            if (removed) {
-+                this.hash -= hash0(element.hashCode());
-+            }
-+
-+            return removed;
-+        }
-+
-+        public boolean contains(final Object element) {
-+            return this.set.contains(element);
-+        }
-+
-+        public E[] getBackingSet() {
-+            return this.set.getRawSet();
-+        }
-+
-+        public int size() {
-+            return this.set.size();
-+        }
-+
-+        @Override
-+        public int hashCode() {
-+            return this.hash;
-+        }
-+
-+        @Override
-+        public boolean equals(final Object other) {
-+            if (!(other instanceof PooledObjectLinkedOpenHashSet)) {
-+                return false;
-+            }
-+            if (this.referenceCount == 0) {
-+                return other == this;
-+            } else {
-+                if (other == this) {
-+                    // Unfortunately we are never equal to our own instance while in use!
-+                    return false;
-+                }
-+                return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set);
-+            }
-+        }
-+
-+        @Override
-+        public String toString() {
-+            return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " +
-+                this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString();
-+        }
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.pooled;
-+
-+import io.papermc.paper.util.MCUtil;
-+import org.apache.commons.lang3.mutable.MutableInt;
-+
-+import java.util.ArrayDeque;
-+import java.util.function.Consumer;
-+import java.util.function.Supplier;
-+
-+public final class PooledObjects<E> {
-+
-+    /**
-+     * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool.
-+     */
-+    public class AutoReleased {
-+        private final E object;
-+        private final Runnable cleaner;
-+
-+        public AutoReleased(E object, Runnable cleaner) {
-+            this.object = object;
-+            this.cleaner = cleaner;
-+        }
-+
-+        public final E getObject() {
-+            return object;
-+        }
-+
-+        public final Runnable getCleaner() {
-+            return cleaner;
-+        }
-+    }
-+
-+    public static final PooledObjects<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024);
-+
-+    private final Supplier<E> creator;
-+    private final Consumer<E> releaser;
-+    private final int maxPoolSize;
-+    private final ArrayDeque<E> queue;
-+
-+    public PooledObjects(final Supplier<E> creator, int maxPoolSize) {
-+        this(creator, maxPoolSize, null);
-+    }
-+    public PooledObjects(final Supplier<E> creator, int maxPoolSize, Consumer<E> releaser) {
-+        if (creator == null) {
-+            throw new NullPointerException("Creator must not be null");
-+        }
-+        if (maxPoolSize <= 0) {
-+            throw new IllegalArgumentException("Max pool size must be greater-than 0");
-+        }
-+
-+        this.queue = new ArrayDeque<>(maxPoolSize);
-+        this.maxPoolSize = maxPoolSize;
-+        this.creator = creator;
-+        this.releaser = releaser;
-+    }
-+
-+    public AutoReleased acquireCleaner(Object holder) {
-+        return acquireCleaner(holder, this::release);
-+    }
-+
-+    public AutoReleased acquireCleaner(Object holder, Consumer<E> releaser) {
-+        E resource = acquire();
-+        Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser);
-+        return new AutoReleased(resource, cleaner);
-+    }
-+
-+    public final E acquire() {
-+        E value;
-+        synchronized (queue) {
-+            value = this.queue.pollLast();
-+        }
-+        return value != null ? value : this.creator.get();
-+    }
-+
-+    public final void release(final E value) {
-+        if (this.releaser != null) {
-+            this.releaser.accept(value);
-+        }
-+        synchronized (this.queue) {
-+            if (queue.size() < this.maxPoolSize) {
-+                this.queue.addLast(value);
-+            }
-+        }
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.util.set;
++package ca.spottedleaf.moonrise.common.set;
 +
 +import java.util.Collection;
 +
-+/**
-+ * @author Spottedleaf &lt;Spottedleaf@users.noreply.github.com>
-+ */
 +public final class OptimizedSmallEnumSet<E extends Enum<E>> {
 +
 +    private final Class<E> enumClass;
@@ -1665,7 +3009,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.enumClass = clazz;
 +    }
 +
-+    public boolean add(final E element) {
++    public boolean addUnchecked(final E element) {
 +        final int ordinal = element.ordinal();
 +        final long key = 1L << ordinal;
 +
@@ -1675,7 +3019,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return (prev & key) == 0;
 +    }
 +
-+    public boolean remove(final E element) {
++    public boolean removeUnchecked(final E element) {
 +        final int ordinal = element.ordinal();
 +        final long key = 1L << ordinal;
 +
@@ -1693,7 +3037,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return Long.bitCount(this.backingSet);
 +    }
 +
-+    public void addAll(final Collection<E> enums) {
++    public void addAllUnchecked(final Collection<E> enums) {
 +        for (final E element : enums) {
 +            if (element == null) {
 +                throw new NullPointerException("Null element");
@@ -1710,63 +3054,38 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return (other.backingSet & this.backingSet) != 0;
 +    }
 +
-+    public boolean contains(final E element) {
++    public boolean hasElement(final E element) {
 +        return (this.backingSet & (1L << element.ordinal())) != 0;
 +    }
 +}
-diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/com/mojang/logging/LogUtils.java
-+++ b/src/main/java/com/mojang/logging/LogUtils.java
-@@ -0,0 +0,0 @@ public class LogUtils {
-     public static Logger getLogger() {
-         return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
-     }
-+    // Paper start
-+    public static Logger getClassLogger() {
-+        return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
-+    }
-+    // Paper end
- }
-diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
 @@ -0,0 +0,0 @@
-+package io.papermc.paper.chunk.system;
++package ca.spottedleaf.moonrise.common.util;
 +
 +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import com.destroystokyo.paper.util.SneakyThrow;
 +import com.mojang.logging.LogUtils;
-+import io.papermc.paper.util.CoordinateUtils;
 +import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ChunkResult;
 +import net.minecraft.server.level.FullChunkStatus;
 +import net.minecraft.server.level.ServerLevel;
 +import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
 +import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
 +import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkPyramid;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
 +import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStep;
-+import org.bukkit.Bukkit;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
 +import org.slf4j.Logger;
-+import java.util.ArrayList;
 +import java.util.List;
-+import java.util.concurrent.CompletableFuture;
 +import java.util.function.Consumer;
 +
 +public final class ChunkSystem {
 +
 +    private static final Logger LOGGER = LogUtils.getLogger();
-+    private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
++    private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
 +
-+    public static int getDistance(final ChunkStatus status) {
++    private static int getDistance(final ChunkStatus status) {
 +        return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
 +    }
 +
@@ -1802,21 +3121,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        });
 +    }
 +
-+    static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
++    static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
 +
 +    private static long chunkLoadCounter = 0L;
 +    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
 +                                         final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+        if (!Bukkit.isPrimaryThread()) {
++        if (!org.bukkit.Bukkit.isPrimaryThread()) {
 +            scheduleChunkTask(level, chunkX, chunkZ, () -> {
 +                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 +            }, priority);
 +            return;
 +        }
 +
-+        final int minLevel = 33 + ChunkSystem.getDistance(toStatus);
++        final int minLevel = 33 + getDistance(toStatus);
 +        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-+        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
 +
 +        if (addTicket) {
 +            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -1830,10 +3149,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +            } catch (final Throwable thr) {
 +                LOGGER.error("Exception handling chunk load callback", thr);
-+                SneakyThrow.sneaky(thr);
++                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
 +            } finally {
 +                if (addTicket) {
-+                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
 +                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
 +                }
 +            }
@@ -1846,14 +3165,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +
-+        final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
++        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
 +
 +        if (loadFuture.isDone()) {
 +            loadCallback.accept(loadFuture.join().orElse(null));
 +            return;
 +        }
 +
-+        loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
++        loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
 +            if (thr != null) {
 +                loadCallback.accept(null);
 +                return;
@@ -1872,7 +3191,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
 +        }
 +
-+        if (!Bukkit.isPrimaryThread()) {
++        if (!org.bukkit.Bukkit.isPrimaryThread()) {
 +            scheduleChunkTask(level, chunkX, chunkZ, () -> {
 +                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 +            }, priority);
@@ -1882,7 +3201,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int minLevel = 33 - (toStatus.ordinal() - 1);
 +        final int radius = toStatus.ordinal() - 1;
 +        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-+        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
 +
 +        if (addTicket) {
 +            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -1896,10 +3215,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +            } catch (final Throwable thr) {
 +                LOGGER.error("Exception handling chunk load callback", thr);
-+                SneakyThrow.sneaky(thr);
++                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
 +            } finally {
 +                if (addTicket) {
-+                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
 +                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
 +                }
 +            }
@@ -1912,7 +3231,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +
-+        final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
++        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
 +        switch (toStatus) {
 +            case FULL: {
 +                tickingState = holder.getFullChunkFuture();
@@ -1936,7 +3255,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +
-+        tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
++        tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
 +            if (thr != null) {
 +                loadCallback.accept(null);
 +                return;
@@ -1948,11 +3267,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
-+        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++        return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
 +    }
 +
 +    public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
-+        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++        return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
 +    }
 +
 +    public static int getVisibleChunkHolderCount(final ServerLevel level) {
@@ -1967,8 +3286,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return getUpdatingChunkHolderCount(level) != 0;
 +    }
 +
-+    public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
-+
++    public static boolean screenEntity(final ServerLevel level, final Entity entity) {
++        return true;
 +    }
 +
 +    public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
@@ -1988,19 +3307,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        chunk.level.getChunkSource().tickingChunks.add(chunk);
++
 +    }
 +
 +    public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        chunk.level.getChunkSource().tickingChunks.remove(chunk);
++
 +    }
 +
 +    public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        chunk.level.getChunkSource().entityTickingChunks.add(chunk);
++
 +    }
 +
 +    public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
++
 +    }
 +
 +    public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
@@ -2014,7 +3333,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static int getLoadViewDistance(final ServerPlayer player) {
 +        final ServerLevel level = player.serverLevel();
 +        if (level == null) {
-+            return Bukkit.getViewDistance();
++            return org.bukkit.Bukkit.getViewDistance();
 +        }
 +        return level.chunkSource.chunkMap.getPlayerViewDistance(player);
 +    }
@@ -2022,51 +3341,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static int getTickViewDistance(final ServerPlayer player) {
 +        final ServerLevel level = player.serverLevel();
 +        if (level == null) {
-+            return Bukkit.getSimulationDistance();
++            return org.bukkit.Bukkit.getSimulationDistance();
 +        }
 +        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
 +    }
 +
-+    private ChunkSystem() {
-+        throw new RuntimeException();
-+    }
++    private ChunkSystem() {}
 +}
-diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/CachedLists.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
 @@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
-+
-+public final class CachedLists {
-+
-+    public static void reset() {
-+
-+    }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
++package ca.spottedleaf.moonrise.common.util;
 +
 +import net.minecraft.core.BlockPos;
 +import net.minecraft.core.SectionPos;
 +import net.minecraft.util.Mth;
 +import net.minecraft.world.entity.Entity;
 +import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.Vec3;
 +
 +public final class CoordinateUtils {
 +
-+    // 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) {
@@ -2157,276 +3455,491 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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 int getBlockX(final Vec3 pos) {
++        return Mth.floor(pos.x);
 +    }
 +
-+    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 int getBlockY(final Vec3 pos) {
++        return Mth.floor(pos.y);
 +    }
 +
-+    public static long getBlockKey(final BlockPos pos) {
-+        return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
++    public static int getBlockZ(final Vec3 pos) {
++        return Mth.floor(pos.z);
 +    }
 +
-+    public static long getBlockKey(final Entity entity) {
-+        return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
++    public static int getChunkX(final Vec3 pos) {
++        return Mth.floor(pos.x) >> 4;
++    }
++
++    public static int getChunkY(final Vec3 pos) {
++        return Mth.floor(pos.y) >> 4;
++    }
++
++    public static int getChunkZ(final Vec3 pos) {
++        return Mth.floor(pos.z) >> 4;
 +    }
 +
 +    private CoordinateUtils() {
 +        throw new RuntimeException();
 +    }
 +}
-diff --git a/src/main/java/io/papermc/paper/util/IntegerUtil.java b/src/main/java/io/papermc/paper/util/IntegerUtil.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 --- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/IntegerUtil.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
 @@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
++package ca.spottedleaf.moonrise.common.util;
 +
-+public final class IntegerUtil {
++import java.util.Objects;
 +
-+    public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
-+    public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
++public final class FlatBitsetUtil {
 +
-+    public static int ceilLog2(final int value) {
-+        return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+    }
++    private static final int LOG2_LONG = 6;
++    private static final long ALL_SET = -1L;
++    private static final int BITS_PER_LONG = Long.SIZE;
 +
-+    public static long ceilLog2(final long value) {
-+        return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+    }
-+
-+    public static int floorLog2(final int value) {
-+        // xor is optimized subtract for 2^n -1
-+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+        return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+    }
-+
-+    public static int floorLog2(final long value) {
-+        // xor is optimized subtract for 2^n -1
-+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+        return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+    }
-+
-+    public static int roundCeilLog2(final int value) {
-+        // optimized variant of 1 << (32 - leading(val - 1))
-+        // given
-+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+        // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+        // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+        // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
-+        // HIGH_BIT_32 >>> (-1 + leading(val - 1))
-+        return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
-+    }
-+
-+    public static long roundCeilLog2(final long value) {
-+        // see logic documented above
-+        return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
-+    }
-+
-+    public static int roundFloorLog2(final int value) {
-+        // optimized variant of 1 << (31 - leading(val))
-+        // given
-+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+        // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+        // HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+        // HIGH_BIT_32 >> (31 - 31 + leading(val))
-+        return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
-+    }
-+
-+    public static long roundFloorLog2(final long value) {
-+        // see logic documented above
-+        return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
-+    }
-+
-+    public static boolean isPowerOfTwo(final int n) {
-+        // 2^n has one bit
-+        // note: this rets true for 0 still
-+        return IntegerUtil.getTrailingBit(n) == n;
-+    }
-+
-+    public static boolean isPowerOfTwo(final long n) {
-+        // 2^n has one bit
-+        // note: this rets true for 0 still
-+        return IntegerUtil.getTrailingBit(n) == n;
-+    }
-+
-+    public static int getTrailingBit(final int n) {
-+        return -n & n;
-+    }
-+
-+    public static long getTrailingBit(final long n) {
-+        return -n & n;
-+    }
-+
-+    public static int trailingZeros(final int n) {
-+        return Integer.numberOfTrailingZeros(n);
-+    }
-+
-+    public static int trailingZeros(final long n) {
-+        return Long.numberOfTrailingZeros(n);
-+    }
-+
-+    // from hacker's delight (signed division magic value)
-+    public static int getDivisorMultiple(final long numbers) {
-+        return (int)(numbers >>> 32);
-+    }
-+
-+    // from hacker's delight (signed division magic value)
-+    public static int getDivisorShift(final long numbers) {
-+        return (int)numbers;
-+    }
-+
-+    // copied from hacker's delight (signed division magic value)
-+    // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
-+    public static long getDivisorNumbers(final int d) {
-+        final int ad = branchlessAbs(d);
-+
-+        if (ad < 2) {
-+            throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
++    // from inclusive
++    // to exclusive
++    public static int firstSet(final long[] bitset, final int from, final int to) {
++        if ((from | to | (to - from)) < 0) {
++            throw new IndexOutOfBoundsException();
 +        }
 +
-+        final int two31 = 0x80000000;
-+        final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
++        int bitsetIdx = from >>> LOG2_LONG;
++        int bitIdx = from & ~(BITS_PER_LONG - 1);
 +
-+        /*
-+         Signed usage:
-+         int number;
-+         long magic = getDivisorNumbers(div);
-+         long mul = magic >>> 32;
-+         int sign = number >> 31;
-+         int result = (int)(((long)number * mul) >>> magic) - sign;
-+         */
-+        /*
-+         Unsigned usage:
-+         int number;
-+         long magic = getDivisorNumbers(div);
-+         long mul = magic >>> 32;
-+         int result = (int)(((long)number * mul) >>> magic);
-+         */
-+
-+        int p = 31;
-+
-+        // all these variables are UNSIGNED!
-+        int t = two31 + (d >>> 31);
-+        int anc = t - 1 - (int)((t & mask)%ad);
-+        int q1 = (int)((two31 & mask)/(anc & mask));
-+        int r1 = two31 - q1*anc;
-+        int q2 = (int)((two31 & mask)/(ad & mask));
-+        int r2 = two31 - q2*ad;
-+        int delta;
-+
-+        do {
-+            p = p + 1;
-+            q1 = 2*q1;                        // Update q1 = 2**p/|nc|.
-+            r1 = 2*r1;                        // Update r1 = rem(2**p, |nc|).
-+            if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
-+                q1 = q1 + 1;
-+                r1 = r1 - anc;
++        long tmp = bitset[bitsetIdx] & (ALL_SET << from);
++        for (;;) {
++            if (tmp != 0L) {
++                final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++                return ret >= to ? -1 : ret;
 +            }
-+            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;
++            bitIdx += BITS_PER_LONG;
++
++            if (bitIdx >= to) {
++                return -1;
++            }
++
++            tmp = bitset[++bitsetIdx];
 +        }
-+        int shift = p;
-+        return ((long)magicNum << 32) | shift;
 +    }
 +
-+    public static int branchlessAbs(final int val) {
-+        // -n = -1 ^ n + 1
-+        final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
-+        return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++    // from inclusive
++    // to exclusive
++    public static int firstClear(final long[] bitset, final int from, final int to) {
++        if ((from | to | (to - from)) < 0) {
++            throw new IndexOutOfBoundsException();
++        }
++        // like firstSet, but invert the bitset
++
++        int bitsetIdx = from >>> LOG2_LONG;
++        int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++        long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
++        for (;;) {
++            if (tmp != 0L) {
++                final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++                return ret >= to ? -1 : ret;
++            }
++
++            bitIdx += BITS_PER_LONG;
++
++            if (bitIdx >= to) {
++                return -1;
++            }
++
++            tmp = ~bitset[++bitsetIdx];
++        }
 +    }
 +
-+    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
++    // from inclusive
++    // to exclusive
++    public static void clearRange(final long[] bitset, final int from, int to) {
++        if ((from | to | (to - from)) < 0) {
++            throw new IndexOutOfBoundsException();
++        }
++
++        if (from == to) {
++            return;
++        }
++
++        --to;
++
++        final int fromBitsetIdx = from >>> LOG2_LONG;
++        final int toBitsetIdx = to >>> LOG2_LONG;
++
++        final long keepFirst = ~(ALL_SET << from);
++        final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
++
++        Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
++
++        if (fromBitsetIdx == toBitsetIdx) {
++            // special case: need to keep both first and last
++            bitset[fromBitsetIdx] &= (keepFirst | keepLast);
++        } else {
++            bitset[fromBitsetIdx] &= keepFirst;
++
++            for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
++                bitset[i] = 0L;
++            }
++
++            bitset[toBitsetIdx] &= keepLast;
++        }
 +    }
 +
-+    //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;
++    // from inclusive
++    // to exclusive
++    public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
++        return firstClear(bitset, from, to) == -1;
 +    }
 +
-+    //score = ~310.01596637036749
-+    public static int hash1(int x) {
-+        x ^= x >>> 15;
-+        x *= 0x356aaaad;
-+        x ^= x >>> 17;
-+        return x;
++
++    private FlatBitsetUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import com.google.gson.JsonElement;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import java.io.File;
++import java.io.FileOutputStream;
++import java.io.IOException;
++import java.io.PrintStream;
++import java.io.StringWriter;
++import java.nio.charset.StandardCharsets;
++
++public final class JsonUtil {
++
++    public static void writeJson(final JsonElement element, final File file) throws IOException {
++        final StringWriter stringWriter = new StringWriter();
++        final JsonWriter jsonWriter = new JsonWriter(stringWriter);
++        jsonWriter.setIndent(" ");
++        jsonWriter.setLenient(false);
++        Streams.write(element, jsonWriter);
++
++        final String jsonString = stringWriter.toString();
++
++        final File parent = file.getParentFile();
++        if (parent != null) {
++            parent.mkdirs();
++        }
++        file.createNewFile();
++        try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
++            out.print(jsonString);
++        }
 +    }
 +
-+    public static int hash2(int x) {
-+        x ^= x >>> 16;
-+        x *= 0x7feb352d;
-+        x ^= x >>> 15;
-+        x *= 0x846ca68b;
-+        x ^= x >>> 16;
-+        return x;
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++public final class MixinWorkarounds {
++
++    // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++    public static long[] clone(final long[] values) {
++        return values.clone();
 +    }
 +
-+    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;
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.io.File;
++
++public final class MoonriseCommon {
++
++    private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
++
++    // Paper start
++    public static PrioritisedThreadPool WORKER_POOL;
++    public static int WORKER_THREADS;
++    public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
++        // Paper end
++        int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
++        if (defaultWorkerThreads <= 4) {
++            defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
++        } else {
++            defaultWorkerThreads = defaultWorkerThreads / 2;
++        }
++        defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); // Paper
++
++        int workerThreads = chunkSystem.workerThreads; // Paper
++
++        if (workerThreads <= 0) {
++            workerThreads = defaultWorkerThreads;
++        }
++
++        WORKER_POOL = new PrioritisedThreadPool(
++                "Paper Worker Pool", workerThreads, // Paper
++                (final Thread thread, final Integer id) -> {
++                    thread.setName("Paper Common Worker #" + id.intValue()); // Paper
++                    thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++                        @Override
++                        public void uncaughtException(final Thread thread, final Throwable throwable) {
++                            LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++                        }
++                    });
++                }, (long)(20.0e6)); // 20ms
++        WORKER_THREADS = workerThreads;
 +    }
 +
-+    //score = ~365.79959673201887
-+    public static long hash1(long x) {
-+        x ^= x >>> 27;
-+        x *= 0xb24924b71d2d354bL;
-+        x ^= x >>> 28;
-+        return x;
++    private MoonriseCommon() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++public final class MoonriseConstants {
++
++    public static final int MAX_VIEW_DISTANCE = 32;
++
++    private MoonriseConstants() {}
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.AABB;
++import net.minecraft.world.phys.Vec3;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++
++import java.util.concurrent.atomic.AtomicInteger;
++
++public class TickThread extends Thread {
++
++    private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class);
++
++    /**
++     * @deprecated
++     */
++    @Deprecated
++    public static void ensureTickThread(final String reason) {
++        if (!isTickThread()) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
 +    }
 +
-+    //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 void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) {
++        if (!isTickThreadFor(world, pos)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
 +    }
 +
-+    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;
++    public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) {
++        if (!isTickThreadFor(world, pos)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
 +    }
 +
-+    private IntegerUtil() {
++    public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
++        if (!isTickThreadFor(world, chunkX, chunkZ)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
++    }
++
++    public static void ensureTickThread(final Entity entity, final String reason) {
++        if (!isTickThreadFor(entity)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
++    }
++
++    public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) {
++        if (!isTickThreadFor(world, aabb)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
++    }
++
++    public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
++        if (!isTickThreadFor(world, blockX, blockZ)) {
++            LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++            throw new IllegalStateException(reason);
++        }
++    }
++
++    public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
++
++    private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
++
++    public TickThread(final String name) {
++        this(null, name);
++    }
++
++    public TickThread(final Runnable run, final String name) {
++        this(run, name, ID_GENERATOR.incrementAndGet());
++    }
++
++    private TickThread(final Runnable run, final String name, final int id) {
++        super(run, name);
++        this.id = id;
++    }
++
++    public static TickThread getCurrentTickThread() {
++        return (TickThread)Thread.currentThread();
++    }
++
++    public static boolean isTickThread() {
++        return org.bukkit.Bukkit.isPrimaryThread(); // Paper
++    }
++
++    public static boolean isShutdownThread() {
++        return false;
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
++        return isTickThread();
++    }
++
++    public static boolean isTickThreadFor(final Entity entity) {
++        return isTickThread();
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelHeightAccessor;
++
++public final class WorldUtil {
++
++    // min, max are inclusive
++
++    public static int getMaxSection(final LevelHeightAccessor world) {
++        return world.getMaxSection() - 1; // getMaxSection() is exclusive
++    }
++
++    public static int getMinSection(final LevelHeightAccessor world) {
++        return world.getMinSection();
++    }
++
++    public static int getMaxLightSection(final LevelHeightAccessor world) {
++        return getMaxSection(world) + 1;
++    }
++
++    public static int getMinLightSection(final LevelHeightAccessor world) {
++        return getMinSection(world) - 1;
++    }
++
++
++
++    public static int getTotalSections(final LevelHeightAccessor world) {
++        return getMaxSection(world) - getMinSection(world) + 1;
++    }
++
++    public static int getTotalLightSections(final LevelHeightAccessor world) {
++        return getMaxLightSection(world) - getMinLightSection(world) + 1;
++    }
++
++    public static int getMinBlockY(final LevelHeightAccessor world) {
++        return getMinSection(world) << 4;
++    }
++
++    public static int getMaxBlockY(final LevelHeightAccessor world) {
++        return (getMaxSection(world) << 4) | 15;
++    }
++
++    public static String getWorldName(final Level world) {
++        if (world == null) {
++            return "null world";
++        }
++        return world.getWorld().getName(); // Paper
++    }
++
++    private WorldUtil() {
 +        throw new RuntimeException();
 +    }
 +}
+diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/mojang/logging/LogUtils.java
++++ b/src/main/java/com/mojang/logging/LogUtils.java
+@@ -0,0 +0,0 @@ public class LogUtils {
+     public static Logger getLogger() {
+         return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
+     }
++    // Paper start
++    public static Logger getClassLogger() {
++        return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
++    }
++    // Paper end
+ }
 diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -2536,6 +4049,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        } else {
 +            // ordered from [head, length)
 +            // then followed by [0, tail)
++
 +            System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
 +            System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
 +
@@ -3147,845 +4661,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return foundFrame.orElse(null);
 +    }
 +}
-diff --git a/src/main/java/io/papermc/paper/util/WorldUtil.java b/src/main/java/io/papermc/paper/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/WorldUtil.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util;
-+
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+    // min, max are inclusive
-+
-+    public static int getMaxSection(final LevelHeightAccessor world) {
-+        return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+    }
-+
-+    public static int getMinSection(final LevelHeightAccessor world) {
-+        return world.getMinSection();
-+    }
-+
-+    public static int getMaxLightSection(final LevelHeightAccessor world) {
-+        return getMaxSection(world) + 1;
-+    }
-+
-+    public static int getMinLightSection(final LevelHeightAccessor world) {
-+        return getMinSection(world) - 1;
-+    }
-+
-+
-+
-+    public static int getTotalSections(final LevelHeightAccessor world) {
-+        return getMaxSection(world) - getMinSection(world) + 1;
-+    }
-+
-+    public static int getTotalLightSections(final LevelHeightAccessor world) {
-+        return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+    }
-+
-+    public static int getMinBlockY(final LevelHeightAccessor world) {
-+        return getMinSection(world) << 4;
-+    }
-+
-+    public static int getMaxBlockY(final LevelHeightAccessor world) {
-+        return (getMaxSection(world) << 4) | 15;
-+    }
-+
-+    private WorldUtil() {
-+        throw new RuntimeException();
-+    }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.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<E> {
-+
-+    public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
-+
-+    protected final Reference2IntLinkedOpenHashMap<E> 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<E> check = new ReferenceOpenHashSet<>();
-+        if (this.listElements != null) {
-+            for (int i = 0; i < this.listSize; ++i) {
-+                Object obj = this.listElements[i];
-+                if (obj != null) {
-+                    iterated++;
-+                    if (!check.add((E)obj)) {
-+                        throw new IllegalStateException("contains duplicate");
-+                    }
-+                    if (!this.contains((E)obj)) {
-+                        throw new IllegalStateException("desync");
-+                    }
-+                }
-+            }
-+        }
-+
-+        if (iterated != this.size()) {
-+            throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
-+        }
-+
-+        check.clear();
-+        iterated = 0;
-+        for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+            final E element = iterator.next();
-+            iterated++;
-+            if (!check.add(element)) {
-+                throw new IllegalStateException("contains duplicate (iterator is wrong)");
-+            }
-+            if (!this.contains(element)) {
-+                throw new IllegalStateException("desync (iterator is wrong)");
-+            }
-+        }
-+
-+        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<Reference2IntMap.Entry<E>> iterator;
-+
-+        if (this.firstInvalidIndex == 0) {
-+            iterator = this.indexMap.reference2IntEntrySet().fastIterator();
-+            lastValidIndex = 0;
-+        } else {
-+            lastValidIndex = this.firstInvalidIndex;
-+            final E key = backingArray[lastValidIndex - 1];
-+            iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
-+                @Override
-+                public int getIntValue() {
-+                    throw new UnsupportedOperationException();
-+                }
-+
-+                @Override
-+                public int setValue(int i) {
-+                    throw new UnsupportedOperationException();
-+                }
-+
-+                @Override
-+                public E getKey() {
-+                    return key;
-+                }
-+            });
-+        }
-+
-+        while (iterator.hasNext()) {
-+            final Reference2IntMap.Entry<E> 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<E> iterator() {
-+        return this.iterator(0);
-+    }
-+
-+    public IteratorSafeOrderedReferenceSet.Iterator<E> 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<E> unsafeIterator() {
-+        return this.unsafeIterator(0);
-+    }
-+    public java.util.Iterator<E> unsafeIterator(final int flags) {
-+        return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
-+    }
-+
-+    public static interface Iterator<E> extends java.util.Iterator<E> {
-+
-+        public void finishedIterating();
-+
-+    }
-+
-+    protected static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
-+
-+        protected final IteratorSafeOrderedReferenceSet<E> 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<E> 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/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util.player;
-+
-+import com.destroystokyo.paper.util.maplist.ReferenceList;
-+import io.papermc.paper.chunk.system.ChunkSystem;
-+import io.papermc.paper.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class NearbyPlayers {
-+
-+    public static enum NearbyMapType {
-+        GENERAL,
-+        GENERAL_SMALL,
-+        GENERAL_REALLY_SMALL,
-+        TICK_VIEW_DISTANCE,
-+        VIEW_DISTANCE;
-+    }
-+
-+    private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values();
-+    public static final int TOTAL_MAP_TYPES = MOB_TYPES.length;
-+
-+    private static final int GENERAL_AREA_VIEW_DISTANCE = 33;
-+    private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
-+    private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
-+
-+    public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
-+    public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
-+    public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
-+
-+    private final ServerLevel world;
-+    private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
-+    private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
-+
-+    public NearbyPlayers(final ServerLevel world) {
-+        this.world = world;
-+    }
-+
-+    public void addPlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
-+        if (this.players.putIfAbsent(player, newTrackers) != null) {
-+            throw new IllegalStateException("Already have player " + player);
-+        }
-+
-+        final ChunkPos chunk = player.chunkPosition();
-+
-+        for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
-+            // use 0 for default, will be updated by tickPlayer
-+            (newTrackers[i] = new TrackedPlayer(player, MOB_TYPES[i])).add(chunk.x, chunk.z, 0);
-+        }
-+
-+        // update view distances
-+        this.tickPlayer(player);
-+    }
-+
-+    public void removePlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] players = this.players.remove(player);
-+        if (players == null) {
-+            return; // May be called during teleportation before the player is actually placed
-+        }
-+
-+        for (final TrackedPlayer tracker : players) {
-+            tracker.remove();
-+        }
-+    }
-+
-+    public void tickPlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] players = this.players.get(player);
-+        if (players == null) {
-+            throw new IllegalStateException("Don't have player " + player);
-+        }
-+
-+        final ChunkPos chunk = player.chunkPosition();
-+
-+        players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
-+        players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
-+        players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
-+        players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
-+        players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
-+    }
-+
-+    public TrackedChunk getChunk(final ChunkPos pos) {
-+        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+    }
-+
-+    public TrackedChunk getChunk(final BlockPos pos) {
-+        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public static final class TrackedChunk {
-+
-+        public final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
-+        private int nonEmptyLists;
-+        private int updateCount;
-+
-+        public boolean isEmpty() {
-+            return this.nonEmptyLists == 0;
-+        }
-+
-+        public int getUpdateCount() {
-+            return this.updateCount;
-+        }
-+
-+        public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
-+            return this.players[type.ordinal()];
-+        }
-+
-+        public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
-+            ++this.updateCount;
-+            final int idx = type.ordinal();
-+            final ReferenceList<ServerPlayer> list = this.players[idx];
-+            if (list == null) {
-+                ++this.nonEmptyLists;
-+                (this.players[idx] = new ReferenceList<>()).add(player);
-+                return;
-+            }
-+
-+            if (!list.add(player)) {
-+                throw new IllegalStateException("Already contains player " + player);
-+            }
-+        }
-+
-+        public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
-+            ++this.updateCount;
-+            final int idx = type.ordinal();
-+            final ReferenceList<ServerPlayer> list = this.players[idx];
-+            if (list == null) {
-+                throw new IllegalStateException("Does not contain player " + player);
-+            }
-+
-+            if (!list.remove(player)) {
-+                throw new IllegalStateException("Does not contain player " + player);
-+            }
-+
-+            if (list.size() == 0) {
-+                this.players[idx] = null;
-+                --this.nonEmptyLists;
-+            }
-+        }
-+    }
-+
-+    private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
-+
-+        final NearbyMapType type;
-+
-+        public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
-+            super(player);
-+            this.type = type;
-+        }
-+
-+        @Override
-+        protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+            NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
-+                return new TrackedChunk();
-+            }).addPlayer(parameter, this.type);
-+        }
-+
-+        @Override
-+        protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+            final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
-+            if (chunk == null) {
-+                throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
-+            }
-+
-+            chunk.removePlayer(parameter, this.type);
-+
-+            if (chunk.isEmpty()) {
-+                NearbyPlayers.this.byChunk.remove(chunkKey);
-+            }
-+        }
-+    }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.util.player;
-+
-+import io.papermc.paper.util.IntegerUtil;
-+
-+public abstract class SingleUserAreaMap<T> {
-+
-+    private static final int NOT_SET = Integer.MIN_VALUE;
-+
-+    private final T parameter;
-+    private int lastChunkX = NOT_SET;
-+    private int lastChunkZ = NOT_SET;
-+    private int distance = NOT_SET;
-+
-+    public SingleUserAreaMap(final T parameter) {
-+        this.parameter = parameter;
-+    }
-+
-+    /* math sign function except 0 returns 1 */
-+    protected static int sign(int val) {
-+        return 1 | (val >> (Integer.SIZE - 1));
-+    }
-+
-+    protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+    protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+    private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+        final int maxX = chunkX + distance;
-+        final int maxZ = chunkZ + distance;
-+
-+        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+                this.addCallback(parameter, cx, cz);
-+            }
-+        }
-+    }
-+
-+    private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+        final int maxX = chunkX + distance;
-+        final int maxZ = chunkZ + distance;
-+
-+        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+                this.removeCallback(parameter, cx, cz);
-+            }
-+        }
-+    }
-+
-+    public final boolean add(final int chunkX, final int chunkZ, final int distance) {
-+        if (distance < 0) {
-+            throw new IllegalArgumentException(Integer.toString(distance));
-+        }
-+        if (this.lastChunkX != NOT_SET) {
-+            return false;
-+        }
-+        this.lastChunkX = chunkX;
-+        this.lastChunkZ = chunkZ;
-+        this.distance = distance;
-+
-+        this.addToNew(this.parameter, chunkX, chunkZ, distance);
-+
-+        return true;
-+    }
-+
-+    public final boolean update(final int toX, final int toZ, final int newViewDistance) {
-+        if (newViewDistance < 0) {
-+            throw new IllegalArgumentException(Integer.toString(newViewDistance));
-+        }
-+        final int fromX = this.lastChunkX;
-+        final int fromZ = this.lastChunkZ;
-+        final int oldViewDistance = this.distance;
-+        if (fromX == NOT_SET) {
-+            return false;
-+        }
-+
-+        this.lastChunkX = toX;
-+        this.lastChunkZ = toZ;
-+        this.distance = newViewDistance;
-+
-+        final T parameter = this.parameter;
-+
-+
-+        final int dx = toX - fromX;
-+        final int dz = toZ - fromZ;
-+
-+        final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
-+
-+        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+            // teleported?
-+            this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
-+            this.addToNew(parameter, toX, toZ, newViewDistance);
-+            return true;
-+        }
-+
-+        if (oldViewDistance != newViewDistance) {
-+            // remove loop
-+
-+            final int oldMinX = fromX - oldViewDistance;
-+            final int oldMinZ = fromZ - oldViewDistance;
-+            final int oldMaxX = fromX + oldViewDistance;
-+            final int oldMaxZ = fromZ + oldViewDistance;
-+            for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
-+                for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
-+
-+                    // only remove if we're outside the new view distance...
-+                    if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
-+                        this.removeCallback(parameter, currX, currZ);
-+                    }
-+                }
-+            }
-+
-+            // add loop
-+
-+            final int newMinX = toX - newViewDistance;
-+            final int newMinZ = toZ - newViewDistance;
-+            final int newMaxX = toX + newViewDistance;
-+            final int newMaxZ = toZ + newViewDistance;
-+            for (int currX = newMinX; currX <= newMaxX; ++currX) {
-+                for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
-+
-+                    // only add if we're outside the old view distance...
-+                    if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
-+                        this.addCallback(parameter, currX, currZ);
-+                    }
-+                }
-+            }
-+
-+            return true;
-+        }
-+
-+        // x axis is width
-+        // z axis is height
-+        // right refers to the x axis of where we moved
-+        // top refers to the z axis of where we moved
-+
-+        // same view distance
-+
-+        // used for relative positioning
-+        final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
-+        final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
-+
-+        // The area excluded by overlapping the two view distance squares creates four rectangles:
-+        // Two on the left, and two on the right. The ones on the left we consider the "removed" section
-+        // and on the right the "added" section.
-+        // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
-+        // exclusive to the regions they surround.
-+
-+        // 4 points of the rectangle
-+        int maxX; // exclusive
-+        int minX; // inclusive
-+        int maxZ; // exclusive
-+        int minZ; // inclusive
-+
-+        if (dx != 0) {
-+            // handle right addition
-+
-+            maxX = toX + (oldViewDistance * right) + right; // exclusive
-+            minX = fromX + (oldViewDistance * right) + right; // inclusive
-+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dz != 0) {
-+            // handle up addition
-+
-+            maxX = toX + (oldViewDistance * right) + right; // exclusive
-+            minX = toX - (oldViewDistance * right); // inclusive
-+            maxZ = toZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = fromZ + (oldViewDistance * up) + up; // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dx != 0) {
-+            // handle left removal
-+
-+            maxX = toX - (oldViewDistance * right); // exclusive
-+            minX = fromX - (oldViewDistance * right); // inclusive
-+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dz != 0) {
-+            // handle down removal
-+
-+            maxX = fromX + (oldViewDistance * right) + right; // exclusive
-+            minX = fromX - (oldViewDistance * right); // inclusive
-+            maxZ = toZ - (oldViewDistance * up); // exclusive
-+            minZ = fromZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        return true;
-+    }
-+
-+    public final boolean remove() {
-+        final int chunkX = this.lastChunkX;
-+        final int chunkZ = this.lastChunkZ;
-+        final int distance = this.distance;
-+        if (chunkX == NOT_SET) {
-+            return false;
-+        }
-+
-+        this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
-+
-+        this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
-+
-+        return true;
-+    }
-+}
 diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/Util.java
@@ -4158,7 +4833,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              SpigotTimings.worldSaveTimer.stopTiming(); // Spigot
          }
 -
-+        io.papermc.paper.util.CachedLists.reset(); // Paper
          this.profiler.push("tallying");
          long j = Util.getNanos() - i;
          int k = this.tickCount % 100;
@@ -4288,7 +4962,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                chunkResult.ifSuccess(chunk -> {
 +                    if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
 +                        ChunkHolder.this.isFullChunkReady = true;
-+                        io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this);
++                        ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
 +                    }
 +                });
 +            });
@@ -4299,7 +4973,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          if (flag && !flag1) {
 +            // Paper start
 +            if (this.isFullChunkReady) {
-+                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 +            }
 +            // Paper end
              this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
@@ -4314,7 +4988,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                chunkResult.ifSuccess(chunk -> {
 +                    // note: Here is a very good place to add callbacks to logic waiting on this.
 +                    ChunkHolder.this.isTickingReady = true;
-+                    io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this);
++                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
 +                });
 +            });
 +            // Paper end
@@ -4325,7 +4999,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
 +            // Paper start
 +            if (this.isTickingReady) {
-+                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 +            }
 +            // Paper end
 +            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
@@ -4340,7 +5014,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.entityTickingChunkFuture.thenAccept(chunkResult -> {
 +                chunkResult.ifSuccess(chunk -> {
 +                    ChunkHolder.this.isEntityTickingReady = true;
-+                    io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this);
++                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
 +                });
 +            });
 +            // Paper end
@@ -4351,7 +5025,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
 +            // Paper start
 +            if (this.isEntityTickingReady) {
-+                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
++                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
 +            }
 +            // Paper end
 +            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
@@ -4385,55 +5059,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      };
      // CraftBukkit end
  
-+    // Paper start - distance maps
-+    private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
-+
-+    void addPlayerToDistanceMaps(ServerPlayer player) {
-+        int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+        int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+        // Note: players need to be explicitly added to distance maps before they can be updated
-+        this.nearbyPlayers.addPlayer(player);
-+    }
-+
-+    void removePlayerFromDistanceMaps(ServerPlayer player) {
-+        int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+        int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+        // Note: players need to be explicitly added to distance maps before they can be updated
-+        this.nearbyPlayers.removePlayer(player);
-+    }
-+
-+    void updateMaps(ServerPlayer player) {
-+        int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+        int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+        // Note: players need to be explicitly added to distance maps before they can be updated
-+        this.nearbyPlayers.tickPlayer(player);
-+    }
-+    // Paper end
 +    // Paper start
 +    public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
-+        return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++        return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
 +    }
-+    public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
 +    // Paper end
 +
      public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
          super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
          this.visibleChunkMap = this.updatingChunkMap.clone();
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-         this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
-         this.setServerViewDistance(viewDistance);
          this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
-+        // Paper start
-+        this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
-+        // Paper end
-+    }
-+
-+    // Paper start
-+    // always use accessor, so folia can override
-+    public final io.papermc.paper.util.player.NearbyPlayers getNearbyPlayers() {
-+        return this.nearbyPlayers;
      }
  
++    // Paper start
 +    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
 +        return -1;
 +    }
@@ -4447,10 +5086,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          stringbuilder.append("Updating:").append(System.lineSeparator());
 -        this.updatingChunkMap.values().forEach(consumer);
-+        io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
++        ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
          stringbuilder.append("Visible:").append(System.lineSeparator());
 -        this.visibleChunkMap.values().forEach(consumer);
-+        io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
++        ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
          CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
          CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
  
@@ -4459,7 +5098,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  } else {
                      holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
 +                    // Paper start
-+                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder);
++                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
 +                    // Paper end
                  }
  
@@ -4474,7 +5113,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      protected void saveAllChunks(boolean flush) {
          if (flush) {
 -            List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
-+            List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
++            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
              MutableBoolean mutableboolean = new MutableBoolean();
  
              do {
@@ -4483,7 +5122,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.flushWorker();
          } else {
 -            this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
-+            io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
++            ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
          }
  
      }
@@ -4492,7 +5131,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public boolean hasWork() {
 -        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
-+        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
++        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
      }
  
      private void processUnloads(BooleanSupplier shouldKeepTicking) {
@@ -4509,7 +5148,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          int l = 0;
 -        ObjectIterator<ChunkHolder> objectiterator = this.visibleChunkMap.values().iterator();
-+        Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
++        Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
  
          while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
              if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
@@ -4521,7 +5160,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                // Paper start
 +                boolean removed;
 +                if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
-+                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
++                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
 +                    // Paper end
                      LevelChunk chunk;
  
@@ -4532,7 +5171,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                      this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
 -                }
 +                } else if (removed) { // Paper start
-+                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
++                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
 +                } // Paper end
  
              }
@@ -4560,7 +5199,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public int size() {
 -        return this.visibleChunkMap.size();
-+        return io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
++        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
      }
  
      public DistanceManager getDistanceManager() {
@@ -4569,14 +5208,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      protected Iterable<ChunkHolder> getChunks() {
 -        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
-+        return Iterables.unmodifiableIterable(io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
++        return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
      }
  
      void dumpChunks(Writer writer) throws IOException {
          CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
          TickingTracker tickingtracker = this.distanceManager.tickingTracker();
 -        ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
-+        Iterator<ChunkHolder> objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
++        Iterator<ChunkHolder> objectbidirectionaliterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
  
          while (objectbidirectionaliterator.hasNext()) {
 -            Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
@@ -4590,30 +5229,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
                  return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- 
-             player.setChunkTrackingView(ChunkTrackingView.EMPTY);
-             this.updateChunkTracking(player);
-+            this.addPlayerToDistanceMaps(player); // Paper - distance maps
-         } else {
-             SectionPos sectionposition = player.getLastSectionPos();
- 
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                 this.distanceManager.removePlayer(sectionposition, player);
-             }
- 
-+            this.removePlayerFromDistanceMaps(player); // Paper - distance maps
-             this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
-         }
- 
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             this.updateChunkTracking(player);
-         }
- 
-+        this.updateMaps(player); // Paper - distance maps
-     }
- 
-     private void updateChunkTracking(ServerPlayer player) {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          });
      }
  
@@ -4675,8 +5290,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @VisibleForDebug
      private NaturalSpawner.SpawnState lastSpawnState;
 +    // Paper start
-+    public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
-+    public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
 +    private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
 +    long chunkFutureAwaitCounter;
 +    // Paper end
@@ -4852,7 +5465,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
 +            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+                io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(
++                ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
 +                    this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
 +                );
 +            }
@@ -4872,19 +5485,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
      // CraftBukkit end
 +    public boolean isRealPlayer; // Paper
-+    public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
  
      public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
          super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
-@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
-         this.updateOptions(clientOptions);
-         this.object = null;
- 
-+        this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
-+
-         // CraftBukkit start
-         this.displayName = this.getScoreboardName();
-         this.bukkitPickUpLoot = true;
 diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/TicketType.java
@@ -5529,8 +6132,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // I don't want to know why this is a generic type.
 +            Entity entityCasted = (Entity)entity;
 +            boolean wasRemoved = entityCasted.isRemoved();
-+            io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd((net.minecraft.server.level.ServerLevel) entityCasted.level(), entityCasted);
-+            if (!wasRemoved && entityCasted.isRemoved()) {
++            boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted);
++            if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
 +                // removed by callback
 +                return false;
 +            }
@@ -5563,7 +6166,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public Chunk[] getLoadedChunks() {
 -        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
 -        return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
-+        List<ChunkHolder> chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
++        List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
 +        return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
      }
  
@@ -5599,7 +6202,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
 +
-+        io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
++        ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
 +            net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
 +                net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
 +                ret.complete(chunk == null ? null : new CraftChunk(chunk));
@@ -5644,7 +6247,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public int getViewDistance() {
-+        return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle());
++        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1;
 +    }
 +
 +    @Override
@@ -5654,7 +6257,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public int getSimulationDistance() {
-+        return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle());
++        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
 +    }
 +
 +    @Override
@@ -5664,7 +6267,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public int getSendViewDistance() {
-+        return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle());
++        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
 +    }
 +
 +    @Override
diff --git a/patches/server/Moonrise-optimisation-patches.patch b/patches/server/Moonrise-optimisation-patches.patch
index 817e80db62..96305715ad 100644
--- a/patches/server/Moonrise-optimisation-patches.patch
+++ b/patches/server/Moonrise-optimisation-patches.patch
@@ -8,3362 +8,351 @@ Currently includes:
  - Entity tracker optimisations
  - Collision optimisations
  - Random block ticking optimisations
+ - Chunk tick iteration optimisations
 
 See https://github.com/Tuinity/Moonrise
 
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
 @@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-+import net.minecraft.world.entity.Entity;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.NoSuchElementException;
-+
-+// list with O(1) remove & contains
-+
-+/**
-+ * @author Spottedleaf
-+ */
-+public final class EntityList implements Iterable<Entity> {
-+
-+    protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
-+    {
-+        this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
-+    }
-+
-+    protected static final Entity[] EMPTY_LIST = new Entity[0];
-+
-+    protected Entity[] entities = EMPTY_LIST;
-+    protected int count;
-+
-+    public int size() {
-+        return this.count;
-+    }
-+
-+    public boolean contains(final Entity entity) {
-+        return this.entityToIndex.containsKey(entity.getId());
-+    }
-+
-+    public boolean remove(final Entity entity) {
-+        final int index = this.entityToIndex.remove(entity.getId());
-+        if (index == Integer.MIN_VALUE) {
-+            return false;
-+        }
-+
-+        // move the entity at the end to this index
-+        final int endIndex = --this.count;
-+        final Entity end = this.entities[endIndex];
-+        if (index != endIndex) {
-+            // not empty after this call
-+            this.entityToIndex.put(end.getId(), index); // update index
-+        }
-+        this.entities[index] = end;
-+        this.entities[endIndex] = null;
-+
-+        return true;
-+    }
-+
-+    public boolean add(final Entity entity) {
-+        final int count = this.count;
-+        final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
-+
-+        if (currIndex != Integer.MIN_VALUE) {
-+            return false; // already in this list
-+        }
-+
-+        Entity[] list = this.entities;
-+
-+        if (list.length == count) {
-+            // resize required
-+            list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+        }
-+
-+        list[count] = entity;
-+        this.count = count + 1;
-+
-+        return true;
-+    }
-+
-+    public Entity getChecked(final int index) {
-+        if (index < 0 || index >= this.count) {
-+            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
-+        }
-+        return this.entities[index];
-+    }
-+
-+    public Entity getUnchecked(final int index) {
-+        return this.entities[index];
-+    }
-+
-+    public Entity[] getRawData() {
-+        return this.entities;
-+    }
-+
-+    public void clear() {
-+        this.entityToIndex.clear();
-+        Arrays.fill(this.entities, 0, this.count, null);
-+        this.count = 0;
-+    }
-+
-+    @Override
-+    public Iterator<Entity> iterator() {
-+        return new Iterator<Entity>() {
-+
-+            Entity lastRet;
-+            int current;
-+
-+            @Override
-+            public boolean hasNext() {
-+                return this.current < EntityList.this.count;
-+            }
-+
-+            @Override
-+            public Entity next() {
-+                if (this.current >= EntityList.this.count) {
-+                    throw new NoSuchElementException();
-+                }
-+                return this.lastRet = EntityList.this.entities[this.current++];
-+            }
-+
-+            @Override
-+            public void remove() {
-+                final Entity lastRet = this.lastRet;
-+
-+                if (lastRet == null) {
-+                    throw new IllegalStateException();
-+                }
-+                this.lastRet = null;
-+
-+                EntityList.this.remove(lastRet);
-+                --this.current;
-+            }
-+        };
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
-+import java.util.Arrays;
-+import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.GlobalPalette;
-+
-+public final class IBlockDataList {
-+
-+    private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
-+
-+    // map of location -> (index | (location << 16) | (palette id << 32))
-+    private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
-+    {
-+        this.map.defaultReturnValue(Long.MAX_VALUE);
-+    }
-+
-+    private static final long[] EMPTY_LIST = new long[0];
-+
-+    private long[] byIndex = EMPTY_LIST;
-+    private int size;
-+
-+    public static int getLocationKey(final int x, final int y, final int z) {
-+        return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
-+    }
-+
-+    public static BlockState getBlockDataFromRaw(final long raw) {
-+        return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
-+    }
-+
-+    public static int getIndexFromRaw(final long raw) {
-+        return (int)(raw & 0xFFFF);
-+    }
-+
-+    public static int getLocationFromRaw(final long raw) {
-+        return (int)((raw >>> 16) & 0xFFFF);
-+    }
-+
-+    public static long getRawFromValues(final int index, final int location, final BlockState data) {
-+        return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
-+    }
-+
-+    public static long setIndexRawValues(final long value, final int index) {
-+        return value & ~(0xFFFF) | (index);
-+    }
-+
-+    public long add(final int x, final int y, final int z, final BlockState data) {
-+        return this.add(getLocationKey(x, y, z), data);
-+    }
-+
-+    public long add(final int location, final BlockState data) {
-+        final long curr = this.map.get((short)location);
-+
-+        if (curr == Long.MAX_VALUE) {
-+            final int index = this.size++;
-+            final long raw = getRawFromValues(index, location, data);
-+            this.map.put((short)location, raw);
-+
-+            if (index >= this.byIndex.length) {
-+                this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
-+            }
-+
-+            this.byIndex[index] = raw;
-+            return raw;
-+        } else {
-+            final int index = getIndexFromRaw(curr);
-+            final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
-+
-+            this.map.put((short)location, raw);
-+
-+            return raw;
-+        }
-+    }
-+
-+    public long remove(final int x, final int y, final int z) {
-+        return this.remove(getLocationKey(x, y, z));
-+    }
-+
-+    public long remove(final int location) {
-+        final long ret = this.map.remove((short)location);
-+        final int index = getIndexFromRaw(ret);
-+        if (ret == Long.MAX_VALUE) {
-+            return ret;
-+        }
-+
-+        // move the entry at the end to this index
-+        final int endIndex = --this.size;
-+        final long end = this.byIndex[endIndex];
-+        if (index != endIndex) {
-+            // not empty after this call
-+            this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
-+        }
-+        this.byIndex[index] = end;
-+        this.byIndex[endIndex] = 0L;
-+
-+        return ret;
-+    }
-+
-+    public int size() {
-+        return this.size;
-+    }
-+
-+    public long getRaw(final int index) {
-+        return this.byIndex[index];
-+    }
-+
-+    public int getLocation(final int index) {
-+        return getLocationFromRaw(this.getRaw(index));
-+    }
-+
-+    public BlockState getData(final int index) {
-+        return getBlockDataFromRaw(this.getRaw(index));
-+    }
-+
-+    public void clear() {
-+        this.size = 0;
-+        this.map.clear();
-+    }
-+
-+    public LongIterator getRawIterator() {
-+        return this.map.values().iterator();
-+    }
-+}
-\ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-+import java.util.Arrays;
-+import java.util.NoSuchElementException;
-+
-+public final class IteratorSafeOrderedReferenceSet<E> {
-+
-+    public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
-+
-+    private final Reference2IntLinkedOpenHashMap<E> indexMap;
-+    private int firstInvalidIndex = -1;
-+
-+    /* list impl */
-+    private E[] listElements;
-+    private int listSize;
-+
-+    private final double maxFragFactor;
-+
-+    private int iteratorCount;
-+
-+    public IteratorSafeOrderedReferenceSet() {
-+        this(16, 0.75f, 16, 0.2);
-+    }
-+
-+    public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
-+                                           final double maxFragFactor) {
-+        this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
-+        this.indexMap.defaultReturnValue(-1);
-+        this.maxFragFactor = maxFragFactor;
-+        this.listElements = (E[])new Object[arrayCapacity];
-+    }
-+
-+    /*
-+    public void check() {
-+        int iterated = 0;
-+        ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
-+        if (this.listElements != null) {
-+            for (int i = 0; i < this.listSize; ++i) {
-+                Object obj = this.listElements[i];
-+                if (obj != null) {
-+                    iterated++;
-+                    if (!check.add((E)obj)) {
-+                        throw new IllegalStateException("contains duplicate");
-+                    }
-+                    if (!this.contains((E)obj)) {
-+                        throw new IllegalStateException("desync");
-+                    }
-+                }
-+            }
-+        }
-+
-+        if (iterated != this.size()) {
-+            throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
-+        }
-+
-+        check.clear();
-+        iterated = 0;
-+        for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+            final E element = iterator.next();
-+            iterated++;
-+            if (!check.add(element)) {
-+                throw new IllegalStateException("contains duplicate (iterator is wrong)");
-+            }
-+            if (!this.contains(element)) {
-+                throw new IllegalStateException("desync (iterator is wrong)");
-+            }
-+        }
-+
-+        if (iterated != this.size()) {
-+            throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
-+        }
-+    }
-+    */
-+
-+    private double getFragFactor() {
-+        return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
-+    }
-+
-+    public int createRawIterator() {
-+        ++this.iteratorCount;
-+        if (this.indexMap.isEmpty()) {
-+            return -1;
-+        } else {
-+            return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
-+        }
-+    }
-+
-+    public int advanceRawIterator(final int index) {
-+        final E[] elements = this.listElements;
-+        int ret = index + 1;
-+        for (int len = this.listSize; ret < len; ++ret) {
-+            if (elements[ret] != null) {
-+                return ret;
-+            }
-+        }
-+
-+        return -1;
-+    }
-+
-+    public void finishRawIterator() {
-+        if (--this.iteratorCount == 0) {
-+            if (this.getFragFactor() >= this.maxFragFactor) {
-+                this.defrag();
-+            }
-+        }
-+    }
-+
-+    public boolean remove(final E element) {
-+        final int index = this.indexMap.removeInt(element);
-+        if (index >= 0) {
-+            if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
-+                this.firstInvalidIndex = index;
-+            }
-+            if (this.listElements[index] != element) {
-+                throw new IllegalStateException();
-+            }
-+            this.listElements[index] = null;
-+            if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
-+                this.defrag();
-+            }
-+            //this.check();
-+            return true;
-+        }
-+        return false;
-+    }
-+
-+    public boolean contains(final E element) {
-+        return this.indexMap.containsKey(element);
-+    }
-+
-+    public boolean add(final E element) {
-+        final int listSize = this.listSize;
-+
-+        final int previous = this.indexMap.putIfAbsent(element, listSize);
-+        if (previous != -1) {
-+            return false;
-+        }
-+
-+        if (listSize >= this.listElements.length) {
-+            this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
-+        }
-+        this.listElements[listSize] = element;
-+        this.listSize = listSize + 1;
-+
-+        //this.check();
-+        return true;
-+    }
-+
-+    private void defrag() {
-+        if (this.firstInvalidIndex < 0) {
-+            return; // nothing to do
-+        }
-+
-+        if (this.indexMap.isEmpty()) {
-+            Arrays.fill(this.listElements, 0, this.listSize, null);
-+            this.listSize = 0;
-+            this.firstInvalidIndex = -1;
-+            //this.check();
-+            return;
-+        }
-+
-+        final E[] backingArray = this.listElements;
-+
-+        int lastValidIndex;
-+        java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
-+
-+        if (this.firstInvalidIndex == 0) {
-+            iterator = this.indexMap.reference2IntEntrySet().fastIterator();
-+            lastValidIndex = 0;
-+        } else {
-+            lastValidIndex = this.firstInvalidIndex;
-+            final E key = backingArray[lastValidIndex - 1];
-+            iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
-+                @Override
-+                public int getIntValue() {
-+                    throw new UnsupportedOperationException();
-+                }
-+
-+                @Override
-+                public int setValue(int i) {
-+                    throw new UnsupportedOperationException();
-+                }
-+
-+                @Override
-+                public E getKey() {
-+                    return key;
-+                }
-+            });
-+        }
-+
-+        while (iterator.hasNext()) {
-+            final Reference2IntMap.Entry<E> 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<E> iterator() {
-+        return this.iterator(0);
-+    }
-+
-+    public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
-+        ++this.iteratorCount;
-+        return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
-+    }
-+
-+    public java.util.Iterator<E> unsafeIterator() {
-+        return this.unsafeIterator(0);
-+    }
-+    public java.util.Iterator<E> unsafeIterator(final int flags) {
-+        return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
-+    }
-+
-+    public static interface Iterator<E> extends java.util.Iterator<E> {
-+
-+        public void finishedIterating();
-+
-+    }
-+
-+    private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
-+
-+        private final IteratorSafeOrderedReferenceSet<E> set;
-+        private final boolean canFinish;
-+        private final int maxIndex;
-+        private int nextIndex;
-+        private E pendingValue;
-+        private boolean finished;
-+        private E lastReturned;
-+
-+        private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
-+            this.set = set;
-+            this.canFinish = canFinish;
-+            this.maxIndex = maxIndex;
-+        }
-+
-+        @Override
-+        public boolean hasNext() {
-+            if (this.finished) {
-+                return false;
-+            }
-+            if (this.pendingValue != null) {
-+                return true;
-+            }
-+
-+            final E[] elements = this.set.listElements;
-+            int index, len;
-+            for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
-+                final E element = elements[index];
-+                if (element != null) {
-+                    this.pendingValue = element;
-+                    this.nextIndex = index + 1;
-+                    return true;
-+                }
-+            }
-+
-+            this.nextIndex = index;
-+            return false;
-+        }
-+
-+        @Override
-+        public E next() {
-+            if (!this.hasNext()) {
-+                throw new NoSuchElementException();
-+            }
-+            final E ret = this.pendingValue;
-+
-+            this.pendingValue = null;
-+            this.lastReturned = ret;
-+
-+            return ret;
-+        }
-+
-+        @Override
-+        public void remove() {
-+            final E lastReturned = this.lastReturned;
-+            if (lastReturned == null) {
-+                throw new IllegalStateException();
-+            }
-+            this.lastReturned = null;
-+            this.set.remove(lastReturned);
-+        }
-+
-+        @Override
-+        public void finishedIterating() {
-+            if (this.finished || !this.canFinish) {
-+                throw new IllegalStateException();
-+            }
-+            this.lastReturned = null;
-+            this.finished = true;
-+            this.set.finishRawIterator();
-+        }
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.NoSuchElementException;
-+
-+public final class ReferenceList<E> implements Iterable<E> {
-+
-+    private final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
-+    {
-+        this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
-+    }
-+
-+    private static final Object[] EMPTY_LIST = new Object[0];
-+
-+    private E[] references;
-+    private int count;
-+
-+    public ReferenceList() {
-+        this((E[])EMPTY_LIST, 0);
-+    }
-+
-+    public ReferenceList(final E[] array, final int count) {
-+        this.references = array;
-+        this.count = count;
-+    }
-+
-+    public int size() {
-+        return this.count;
-+    }
-+
-+    public boolean contains(final E obj) {
-+        return this.referenceToIndex.containsKey(obj);
-+    }
-+
-+    public boolean remove(final E obj) {
-+        final int index = this.referenceToIndex.removeInt(obj);
-+        if (index == Integer.MIN_VALUE) {
-+            return false;
-+        }
-+
-+        // move the object at the end to this index
-+        final int endIndex = --this.count;
-+        final E end = (E)this.references[endIndex];
-+        if (index != endIndex) {
-+            // not empty after this call
-+            this.referenceToIndex.put(end, index); // update index
-+        }
-+        this.references[index] = end;
-+        this.references[endIndex] = null;
-+
-+        return true;
-+    }
-+
-+    public boolean add(final E obj) {
-+        final int count = this.count;
-+        final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
-+
-+        if (currIndex != Integer.MIN_VALUE) {
-+            return false; // already in this list
-+        }
-+
-+        E[] list = this.references;
-+
-+        if (list.length == count) {
-+            // resize required
-+            list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+        }
-+
-+        list[count] = obj;
-+        this.count = count + 1;
-+
-+        return true;
-+    }
-+
-+    public E getChecked(final int index) {
-+        if (index < 0 || index >= this.count) {
-+            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
-+        }
-+        return this.references[index];
-+    }
-+
-+    public E getUnchecked(final int index) {
-+        return this.references[index];
-+    }
-+
-+    public Object[] getRawData() {
-+        return this.references;
-+    }
-+
-+    public E[] getRawDataUnchecked() {
-+        return this.references;
-+    }
-+
-+    public void clear() {
-+        this.referenceToIndex.clear();
-+        Arrays.fill(this.references, 0, this.count, null);
-+        this.count = 0;
-+    }
-+
-+    @Override
-+    public Iterator<E> iterator() {
-+        return new Iterator<>() {
-+            private E lastRet;
-+            private int current;
-+
-+            @Override
-+            public boolean hasNext() {
-+                return this.current < ReferenceList.this.count;
-+            }
-+
-+            @Override
-+            public E next() {
-+                if (this.current >= ReferenceList.this.count) {
-+                    throw new NoSuchElementException();
-+                }
-+                return this.lastRet = ReferenceList.this.references[this.current++];
-+            }
-+
-+            @Override
-+            public void remove() {
-+                final E lastRet = this.lastRet;
-+
-+                if (lastRet == null) {
-+                    throw new IllegalStateException();
-+                }
-+                this.lastRet = null;
-+
-+                ReferenceList.this.remove(lastRet);
-+                --this.current;
-+            }
-+        };
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import java.lang.reflect.Array;
-+import java.util.Arrays;
-+import java.util.Comparator;
-+
-+public final class SortedList<E> {
-+
-+    private static final Object[] EMPTY_LIST = new Object[0];
-+
-+    private Comparator<? super E> comparator;
-+    private E[] elements;
-+    private int count;
-+
-+    public SortedList(final Comparator<? super E> comparator) {
-+        this((E[])EMPTY_LIST, comparator);
-+    }
-+
-+    public SortedList(final E[] elements, final Comparator<? super E> comparator) {
-+        this.elements = elements;
-+        this.comparator = comparator;
-+    }
-+
-+    // start, end are inclusive
-+    private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> comparator,
-+                                     int start, int end) {
-+        while (start <= end) {
-+            final int middle = (start + end) >>> 1;
-+
-+            final E middleVal = elements[middle];
-+
-+            final int cmp = comparator.compare(element, middleVal);
-+
-+            if (cmp < 0) {
-+                end = middle - 1;
-+            } else {
-+                start = middle + 1;
-+            }
-+        }
-+
-+        return start;
-+    }
-+
-+    public int size() {
-+        return this.count;
-+    }
-+
-+    public boolean isEmpty() {
-+        return this.count == 0;
-+    }
-+
-+    public int add(final E element) {
-+        E[] elements = this.elements;
-+        final int count = this.count;
-+        this.count = count + 1;
-+        final Comparator<? super E> comparator = this.comparator;
-+
-+        final int idx = insertIdx(elements, element, comparator, 0, count - 1);
-+
-+        if (count >= elements.length) {
-+            // copy and insert at the same time
-+            if (idx == count) {
-+                this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+                elements[count] = element;
-+                return idx;
-+            } else {
-+                final E[] newElements = (E[])Array.newInstance(elements.getClass().getComponentType(), (int)Math.max(4L, count * 2L));
-+                System.arraycopy(elements, 0, newElements, 0, idx);
-+                newElements[idx] = element;
-+                System.arraycopy(elements, idx, newElements, idx + 1, count - idx);
-+                this.elements = newElements;
-+                return idx;
-+            }
-+        } else {
-+            if (idx == count) {
-+                // no copy needed
-+                elements[idx] = element;
-+                return idx;
-+            } else {
-+                // shift elements down
-+                System.arraycopy(elements, idx, elements, idx + 1, count - idx);
-+                elements[idx] = element;
-+                return idx;
-+            }
-+        }
-+    }
-+
-+    public E get(final int idx) {
-+        if (idx < 0 || idx >= this.count) {
-+            throw new IndexOutOfBoundsException(idx);
-+        }
-+        return this.elements[idx];
-+    }
-+
-+
-+    public E remove(final E element) {
-+        E[] elements = this.elements;
-+        final int count = this.count;
-+        final Comparator<? super E> comparator = this.comparator;
-+
-+        final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
-+        if (idx < 0) {
-+            return null;
-+        }
-+
-+        final int last = this.count - 1;
-+        this.count = last;
-+
-+        final E ret = elements[idx];
-+
-+        System.arraycopy(elements, idx + 1, elements, idx, last - idx);
-+
-+        elements[last] = null;
-+
-+        return ret;
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import it.unimi.dsi.fastutil.ints.Int2IntFunction;
-+
-+import java.util.Arrays;
-+
-+public class Int2IntArraySortedMap {
-+
-+    protected int[] key;
-+    protected int[] val;
-+    protected int size;
-+
-+    public Int2IntArraySortedMap() {
-+        this.key = new int[8];
-+        this.val = new int[8];
-+    }
-+
-+    public int put(final int key, final int value) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            final int current = this.val[index];
-+            this.val[index] = value;
-+            return current;
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+        this.val[insert] = value;
-+
-+        return 0;
-+    }
-+
-+    public int computeIfAbsent(final int key, final Int2IntFunction producer) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            return this.val[index];
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+
-+        return this.val[insert] = producer.apply(key);
-+    }
-+
-+    public int get(final int key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            return 0;
-+        }
-+        return this.val[index];
-+    }
-+
-+    public int getFloor(final int key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            final int insert = -(index + 1) - 1;
-+            return insert < 0 ? 0 : this.val[insert];
-+        }
-+        return this.val[index];
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import java.util.Arrays;
-+import java.util.function.IntFunction;
-+
-+public class Int2ObjectArraySortedMap<V> {
-+
-+    protected int[] key;
-+    protected V[] val;
-+    protected int size;
-+
-+    public Int2ObjectArraySortedMap() {
-+        this.key = new int[8];
-+        this.val = (V[])new Object[8];
-+    }
-+
-+    public V put(final int key, final V value) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            final V current = this.val[index];
-+            this.val[index] = value;
-+            return current;
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+
-+        this.key[insert] = key;
-+        this.val[insert] = value;
-+
-+        return null;
-+    }
-+
-+    public V computeIfAbsent(final int key, final IntFunction<V> producer) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            return this.val[index];
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+
-+        this.key[insert] = key;
-+
-+        return this.val[insert] = producer.apply(key);
-+    }
-+
-+    public V get(final int key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            return null;
-+        }
-+        return this.val[index];
-+    }
-+
-+    public V getFloor(final int key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            final int insert = -(index + 1);
-+            return this.val[insert];
-+        }
-+        return this.val[index];
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import it.unimi.dsi.fastutil.longs.Long2IntFunction;
-+
-+import java.util.Arrays;
-+
-+public class Long2IntArraySortedMap {
-+
-+    protected long[] key;
-+    protected int[] val;
-+    protected int size;
-+
-+    public Long2IntArraySortedMap() {
-+        this.key = new long[8];
-+        this.val = new int[8];
-+    }
-+
-+    public int put(final long key, final int value) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            final int current = this.val[index];
-+            this.val[index] = value;
-+            return current;
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+        this.val[insert] = value;
-+
-+        return 0;
-+    }
-+
-+    public int computeIfAbsent(final long key, final Long2IntFunction producer) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            return this.val[index];
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+
-+        return this.val[insert] = producer.apply(key);
-+    }
-+
-+    public int get(final long key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            return 0;
-+        }
-+        return this.val[index];
-+    }
-+
-+    public int getFloor(final long key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            final int insert = -(index + 1) - 1;
-+            return insert < 0 ? 0 : this.val[insert];
-+        }
-+        return this.val[index];
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import java.util.Arrays;
-+import java.util.function.LongFunction;
-+
-+public class Long2ObjectArraySortedMap<V> {
-+
-+    protected long[] key;
-+    protected V[] val;
-+    protected int size;
-+
-+    public Long2ObjectArraySortedMap() {
-+        this.key = new long[8];
-+        this.val = (V[])new Object[8];
-+    }
-+
-+    public V put(final long key, final V value) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            final V current = this.val[index];
-+            this.val[index] = value;
-+            return current;
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+        this.val[insert] = value;
-+
-+        return null;
-+    }
-+
-+    public V computeIfAbsent(final long key, final LongFunction<V> producer) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index >= 0) {
-+            return this.val[index];
-+        }
-+        final int insert = -(index + 1);
-+        // shift entries down
-+        if (this.size >= this.val.length) {
-+            this.key = Arrays.copyOf(this.key, this.key.length * 2);
-+            this.val = Arrays.copyOf(this.val, this.val.length * 2);
-+        }
-+        System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
-+        System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
-+        ++this.size;
-+
-+        this.key[insert] = key;
-+
-+        return this.val[insert] = producer.apply(key);
-+    }
-+
-+    public V get(final long key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            return null;
-+        }
-+        return this.val[index];
-+    }
-+
-+    public V getFloor(final long key) {
-+        final int index = Arrays.binarySearch(this.key, 0, this.size, key);
-+        if (index < 0) {
-+            final int insert = -(index + 1) - 1;
-+            return insert < 0 ? null : this.val[insert];
-+        }
-+        return this.val[index];
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
-+import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
-+
-+public final class SynchronisedLong2BooleanMap {
-+    private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
-+    private final int limit;
-+
-+    public SynchronisedLong2BooleanMap(final int limit) {
-+        this.limit = limit;
-+    }
-+
-+    // must hold lock on map
-+    private void purgeEntries() {
-+        while (this.map.size() > this.limit) {
-+            this.map.removeLastBoolean();
-+        }
-+    }
-+
-+    public boolean remove(final long key) {
-+        synchronized (this.map) {
-+            return this.map.remove(key);
-+        }
-+    }
-+
-+    // note:
-+    public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
-+        synchronized (this.map) {
-+            if (this.map.containsKey(key)) {
-+                return this.map.getAndMoveToFirst(key);
-+            }
-+        }
-+
-+        final boolean put = ifAbsent.get(key);
-+
-+        synchronized (this.map) {
-+            if (this.map.containsKey(key)) {
-+                return this.map.getAndMoveToFirst(key);
-+            }
-+            this.map.putAndMoveToFirst(key, put);
-+
-+            this.purgeEntries();
-+
-+            return put;
-+        }
-+    }
-+}
-\ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.map;
-+
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import java.util.function.BiFunction;
-+
-+public final class SynchronisedLong2ObjectMap<V> {
-+    private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
-+    private final int limit;
-+
-+    public SynchronisedLong2ObjectMap(final int limit) {
-+        this.limit = limit;
-+    }
-+
-+    // must hold lock on map
-+    private void purgeEntries() {
-+        while (this.map.size() > this.limit) {
-+            this.map.removeLast();
-+        }
-+    }
-+
-+    public V get(final long key) {
-+        synchronized (this.map) {
-+            return this.map.getAndMoveToFirst(key);
-+        }
-+    }
-+
-+    public V put(final long key, final V value) {
-+        synchronized (this.map) {
-+            final V ret = this.map.putAndMoveToFirst(key, value);
-+            this.purgeEntries();
-+            return ret;
-+        }
-+    }
-+
-+    public V compute(final long key, final BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
-+        synchronized (this.map) {
-+            // first, compute the value - if one is added, it will be at the last entry
-+            this.map.compute(key, remappingFunction);
-+            // move the entry to first, just in case it was added at last
-+            final V ret = this.map.getAndMoveToFirst(key);
-+            // now purge the last entries
-+            this.purgeEntries();
-+
-+            return ret;
-+        }
-+    }
-+}
-\ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+public final class AllocatingRateLimiter {
-+
-+    // max difference granularity in ns
-+    private final long maxGranularity;
-+
-+    private double allocation = 0.0;
-+    private long lastAllocationUpdate;
-+    // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error)
-+    // over any time period using take regardless of the number of take calls or the intervals between the take calls
-+    // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3
-+    private double takeCarry = 0.0;
-+    private long lastTakeUpdate;
-+
-+    public AllocatingRateLimiter(final long maxGranularity) {
-+        this.maxGranularity = maxGranularity;
-+    }
-+
-+    public void reset(final long time) {
-+        this.allocation = 0.0;
-+        this.lastAllocationUpdate = time;
-+        this.takeCarry = 0.0;
-+        this.lastTakeUpdate = time;
-+    }
-+
-+    // rate in units/s, and time in ns
-+    public void tickAllocation(final long time, final double rate, final double maxAllocation) {
-+        final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate);
-+        this.lastAllocationUpdate = time;
-+
-+        this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
-+    }
-+
-+    public long previewAllocation(final long time, final double rate, final long maxTake) {
-+        if (maxTake < 1L) {
-+            return 0L;
-+        }
-+
-+        final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
-+
-+        // note: abs(takeCarry) <= 1.0
-+        final double take = Math.min(
-+            Math.min((double)maxTake - this.takeCarry, this.allocation),
-+            rate * (diff*1.0E-9)
+ package ca.spottedleaf.moonrise.common.util;
+ 
+ import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache;
+ import com.mojang.logging.LogUtils;
+ import net.minecraft.server.level.ChunkHolder;
+ import net.minecraft.server.level.FullChunkStatus;
+@@ -0,0 +0,0 @@ import java.util.function.Consumer;
+ public final class ChunkSystem {
+ 
+     private static final Logger LOGGER = LogUtils.getLogger();
+-    private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+-
+-    private static int getDistance(final ChunkStatus status) {
+-        return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
+-    }
+ 
+     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+         scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+     }
+ 
+     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
+-        level.chunkSource.mainThreadProcessor.execute(run);
++        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
+     }
+ 
+     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+                                          final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
+                                          final Consumer<ChunkAccess> onComplete) {
+-        if (gen) {
+-            scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-            return;
+-        }
+-        scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+-            if (chunk == null) {
+-                if (onComplete != null) {
+-                    onComplete.accept(null);
+-                }
+-            } else {
+-                if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+-                    scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-                } else {
+-                    if (onComplete != null) {
+-                        onComplete.accept(null);
+-                    }
+-                }
+-            }
+-        });
++        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
+     }
+ 
+-    static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
+-
+-    private static long chunkLoadCounter = 0L;
+     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+                                          final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
+-        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+-            scheduleChunkTask(level, chunkX, chunkZ, () -> {
+-                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-            }, priority);
+-            return;
+-        }
+-
+-        final int minLevel = 33 + getDistance(toStatus);
+-        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+-        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+-
+-        if (addTicket) {
+-            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+-        }
+-        level.chunkSource.runDistanceManagerUpdates();
+-
+-        final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+-            try {
+-                if (onComplete != null) {
+-                    onComplete.accept(chunk);
+-                }
+-            } catch (final Throwable thr) {
+-                LOGGER.error("Exception handling chunk load callback", thr);
+-                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+-            } finally {
+-                if (addTicket) {
+-                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+-                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+-                }
+-            }
+-        };
+-
+-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-
+-        if (holder == null || holder.getTicketLevel() > minLevel) {
+-            loadCallback.accept(null);
+-            return;
+-        }
+-
+-        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
+-
+-        if (loadFuture.isDone()) {
+-            loadCallback.accept(loadFuture.join().orElse(null));
+-            return;
+-        }
+-
+-        loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
+-            if (thr != null) {
+-                loadCallback.accept(null);
+-                return;
+-            }
+-            loadCallback.accept(result.orElse(null));
+-        }, (final Runnable r) -> {
+-            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+-        });
++        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+     }
+ 
+     public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+                                             final FullChunkStatus toStatus, final boolean addTicket,
+                                             final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
+-        // This method goes unused until the chunk system rewrite
+-        if (toStatus == FullChunkStatus.INACCESSIBLE) {
+-            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+-        }
+-
+-        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+-            scheduleChunkTask(level, chunkX, chunkZ, () -> {
+-                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+-            }, priority);
+-            return;
+-        }
+-
+-        final int minLevel = 33 - (toStatus.ordinal() - 1);
+-        final int radius = toStatus.ordinal() - 1;
+-        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+-        final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+-
+-        if (addTicket) {
+-            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+-        }
+-        level.chunkSource.runDistanceManagerUpdates();
+-
+-        final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+-            try {
+-                if (onComplete != null) {
+-                    onComplete.accept(chunk);
+-                }
+-            } catch (final Throwable thr) {
+-                LOGGER.error("Exception handling chunk load callback", thr);
+-                com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+-            } finally {
+-                if (addTicket) {
+-                    level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+-                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+-                }
+-            }
+-        };
+-
+-        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-
+-        if (holder == null || holder.getTicketLevel() > minLevel) {
+-            loadCallback.accept(null);
+-            return;
+-        }
+-
+-        final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
+-        switch (toStatus) {
+-            case FULL: {
+-                tickingState = holder.getFullChunkFuture();
+-                break;
+-            }
+-            case BLOCK_TICKING: {
+-                tickingState = holder.getTickingChunkFuture();
+-                break;
+-            }
+-            case ENTITY_TICKING: {
+-                tickingState = holder.getEntityTickingChunkFuture();
+-                break;
+-            }
+-            default: {
+-                throw new IllegalStateException("Cannot reach here");
+-            }
+-        }
+-
+-        if (tickingState.isDone()) {
+-            loadCallback.accept(tickingState.join().orElse(null));
+-            return;
+-        }
+-
+-        tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
+-            if (thr != null) {
+-                loadCallback.accept(null);
+-                return;
+-            }
+-            loadCallback.accept(result.orElse(null));
+-        }, (final Runnable r) -> {
+-            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+-        });
++        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+     }
+ 
+     public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+-        return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+     }
+ 
+     public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+-        return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+     }
+ 
+     public static int getVisibleChunkHolderCount(final ServerLevel level) {
+-        return level.chunkSource.chunkMap.visibleChunkMap.size();
++        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
+     }
+ 
+     public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+-        return level.chunkSource.chunkMap.updatingChunkMap.size();
++        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
+     }
+ 
+     public static boolean hasAnyChunkHolders(final ServerLevel level) {
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+ 
+     }
+ 
+-    public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++    public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
++        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
++                .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
++    }
+ 
++    public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
-+
-+        return (long)Math.floor(this.takeCarry + take);
-+    }
-+
-+    // rate in units/s, and time in ns
-+    public long takeAllocation(final long time, final double rate, final long maxTake) {
-+        if (maxTake < 1L) {
-+            return 0L;
-+        }
-+
-+        double ret = this.takeCarry;
-+        final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
-+        this.lastTakeUpdate = time;
-+
-+        // note: abs(takeCarry) <= 1.0
-+        final double take = Math.min(
-+            Math.min((double)maxTake - this.takeCarry, this.allocation),
-+            rate * (diff*1.0E-9)
+     }
+ 
+     public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
 +        );
-+
-+        ret += take;
-+        this.allocation -= take;
-+
-+        final long retInteger = (long)Math.floor(ret);
-+        this.takeCarry = ret - (double)retInteger;
-+
-+        return retInteger;
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+
-+public final class Delayed26WayDistancePropagator3D {
-+
-+    // this map is considered "stale" unless updates are propagated.
-+    protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
-+
-+    // this map is never stale
-+    protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
-+
-+    // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
-+    // propagating updates
-+    protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
-+
-+    @FunctionalInterface
-+    public static interface LevelChangeCallback {
-+
-+        /**
-+         * This can be called for intermediate updates. So do not rely on newLevel being close to or
-+         * the exact level that is expected after a full propagation has occured.
-+         */
-+        public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
-+
-+    }
-+
-+    protected final LevelChangeCallback changeCallback;
-+
-+    public Delayed26WayDistancePropagator3D() {
-+        this(null);
-+    }
-+
-+    public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
-+        this.changeCallback = changeCallback;
-+    }
-+
-+    public int getLevel(final long pos) {
-+        return this.levels.get(pos);
-+    }
-+
-+    public int getLevel(final int x, final int y, final int z) {
-+        return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
-+    }
-+
-+    public void setSource(final int x, final int y, final int z, final int level) {
-+        this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
-+    }
-+
-+    public void setSource(final long coordinate, final int level) {
-+        if ((level & 63) != level || level == 0) {
-+            throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
-+        }
-+
-+        final byte byteLevel = (byte)level;
-+        final byte oldLevel = this.sources.put(coordinate, byteLevel);
-+
-+        if (oldLevel == byteLevel) {
-+            return; // nothing to do
-+        }
-+
-+        // queue to update later
-+        this.updatedSources.add(coordinate);
-+    }
-+
-+    public void removeSource(final int x, final int y, final int z) {
-+        this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
-+    }
-+
-+    public void removeSource(final long coordinate) {
-+        if (this.sources.remove(coordinate) != 0) {
-+            this.updatedSources.add(coordinate);
-+        }
-+    }
-+
-+    // queues used for BFS propagating levels
-+    protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
-+    {
-+        for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
-+            this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
-+        }
-+    }
-+    protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
-+    {
-+        for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
-+            this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
-+        }
-+    }
-+    protected long levelIncreaseWorkQueueBitset;
-+    protected long levelRemoveWorkQueueBitset;
-+
-+    protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
-+        final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelIncreaseWorkQueueBitset |= (1L << level);
-+    }
-+
-+    protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
-+        final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelIncreaseWorkQueueBitset |= (1L << index);
-+    }
-+
-+    protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
-+        final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelRemoveWorkQueueBitset |= (1L << level);
-+    }
-+
-+    public boolean propagateUpdates() {
-+        if (this.updatedSources.isEmpty()) {
-+            return false;
-+        }
-+
-+        boolean ret = false;
-+
-+        for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
-+            final long coordinate = iterator.nextLong();
-+
-+            final byte currentLevel = this.levels.get(coordinate);
-+            final byte updatedSource = this.sources.get(coordinate);
-+
-+            if (currentLevel == updatedSource) {
-+                continue;
-+            }
-+            ret = true;
-+
-+            if (updatedSource > currentLevel) {
-+                // level increase
-+                this.addToIncreaseWorkQueue(coordinate, updatedSource);
-+            } else {
-+                // level decrease
-+                this.addToRemoveWorkQueue(coordinate, currentLevel);
-+                // if the current coordinate is a source, then the decrease propagation will detect that and queue
-+                // the source propagation
-+            }
-+        }
-+
-+        this.updatedSources.clear();
-+
-+        // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
-+        // make the removes remove less)
-+        this.propagateIncreases();
-+
-+        // now we propagate the decreases (which will then re-propagate clobbered sources)
-+        this.propagateDecreases();
-+
-+        return ret;
-+    }
-+
-+    protected void propagateIncreases() {
-+        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
-+             this.levelIncreaseWorkQueueBitset != 0L;
-+             this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
-+
-+            final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
-+            while (!queue.queuedLevels.isEmpty()) {
-+                final long coordinate = queue.queuedCoordinates.removeFirstLong();
-+                byte level = queue.queuedLevels.removeFirstByte();
-+
-+                final boolean neighbourCheck = level < 0;
-+
-+                final byte currentLevel;
-+                if (neighbourCheck) {
-+                    level = (byte)-level;
-+                    currentLevel = this.levels.get(coordinate);
-+                } else {
-+                    currentLevel = this.levels.putIfGreater(coordinate, level);
-+                }
-+
-+                if (neighbourCheck) {
-+                    // used when propagating from decrease to indicate that this level needs to check its neighbours
-+                    // this means the level at coordinate could be equal, but would still need neighbours checked
-+
-+                    if (currentLevel != level) {
-+                        // something caused the level to change, which means something propagated to it (which means
-+                        // us propagating here is redundant), or something removed the level (which means we
-+                        // cannot propagate further)
-+                        continue;
-+                    }
-+                } else if (currentLevel >= level) {
-+                    // something higher/equal propagated
-+                    continue;
-+                }
-+                if (this.changeCallback != null) {
-+                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
-+                }
-+
-+                if (level == 1) {
-+                    // can't propagate 0 to neighbours
-+                    continue;
-+                }
-+
-+                // propagate to neighbours
-+                final byte neighbourLevel = (byte)(level - 1);
-+                final int x = CoordinateUtils.getChunkSectionX(coordinate);
-+                final int y = CoordinateUtils.getChunkSectionY(coordinate);
-+                final int z = CoordinateUtils.getChunkSectionZ(coordinate);
-+
-+                for (int dy = -1; dy <= 1; ++dy) {
-+                    for (int dz = -1; dz <= 1; ++dz) {
-+                        for (int dx = -1; dx <= 1; ++dx) {
-+                            if ((dy | dz | dx) == 0) {
-+                                // already propagated to coordinate
-+                                continue;
-+                            }
-+
-+                            // sure we can check the neighbour level in the map right now and avoid a propagation,
-+                            // but then we would still have to recheck it when popping the value off of the queue!
-+                            // so just avoid the double lookup
-+                            final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
-+                            this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
-+                        }
-+                    }
-+                }
-+            }
-+        }
-+    }
-+
-+    protected void propagateDecreases() {
-+        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
-+             this.levelRemoveWorkQueueBitset != 0L;
-+             this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
-+
-+            final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
-+            while (!queue.queuedLevels.isEmpty()) {
-+                final long coordinate = queue.queuedCoordinates.removeFirstLong();
-+                final byte level = queue.queuedLevels.removeFirstByte();
-+
-+                final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
-+                if (currentLevel == 0) {
-+                    // something else removed
-+                    continue;
-+                }
-+
-+                if (currentLevel > level) {
-+                    // something higher propagated here or we hit the propagation of another source
-+                    // in the second case we need to re-propagate because we could have just clobbered another source's
-+                    // propagation
-+                    this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
-+                    continue;
-+                }
-+
-+                if (this.changeCallback != null) {
-+                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
-+                }
-+
-+                final byte source = this.sources.get(coordinate);
-+                if (source != 0) {
-+                    // must re-propagate source later
-+                    this.addToIncreaseWorkQueue(coordinate, source);
-+                }
-+
-+                if (level == 0) {
-+                    // can't propagate -1 to neighbours
-+                    // we have to check neighbours for removing 1 just in case the neighbour is 2
-+                    continue;
-+                }
-+
-+                // propagate to neighbours
-+                final byte neighbourLevel = (byte)(level - 1);
-+                final int x = CoordinateUtils.getChunkSectionX(coordinate);
-+                final int y = CoordinateUtils.getChunkSectionY(coordinate);
-+                final int z = CoordinateUtils.getChunkSectionZ(coordinate);
-+
-+                for (int dy = -1; dy <= 1; ++dy) {
-+                    for (int dz = -1; dz <= 1; ++dz) {
-+                        for (int dx = -1; dx <= 1; ++dx) {
-+                            if ((dy | dz | dx) == 0) {
-+                                // already propagated to coordinate
-+                                continue;
-+                            }
-+
-+                            // sure we can check the neighbour level in the map right now and avoid a propagation,
-+                            // but then we would still have to recheck it when popping the value off of the queue!
-+                            // so just avoid the double lookup
-+                            final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
-+                            this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
-+                        }
-+                    }
-+                }
-+            }
-+        }
-+
-+        // propagate sources we clobbered in the process
-+        this.propagateIncreases();
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.HashCommon;
-+import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
-+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+
-+public final class Delayed8WayDistancePropagator2D {
-+
-+    // Test
-+    /*
-+    protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) {
-+        int got = test.getLevel(x, z);
-+
-+        int expect = 0;
-+        Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet();
-+        if (nearest != null) {
-+            for (Object _obj : nearest) {
-+                if (_obj instanceof Ticket) {
-+                    Ticket ticket = (Ticket)_obj;
-+                    long ticketCoord = reference.getLastCoordinate(ticket);
-+                    int viewDistance = reference.getLastViewDistance(ticket);
-+                    int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x),
-+                            com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z));
-+                    int level = viewDistance - distance;
-+                    if (level > expect) {
-+                        expect = level;
-+                    }
-+                }
-+            }
-+        }
-+
-+        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<Ticket> empty
-+                = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
-+
-+    }
-+
-+    public static void main(final String[] args) {
-+        com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
-+            @Override
-+            protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) {
-+                return object.empty;
-+            }
-+        };
-+        Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
-+
-+        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<Ticket> list = new java.util.ArrayList<>();
-+            for (int i = 0; i < 10; ++i) {
-+                Ticket a = new Ticket();
-+                list.add(a);
-+                a.x = (i & 1) == 1 ? -i : i;
-+                a.z = (i & 1) == 1 ? -i : i;
-+            }
-+            for (Ticket ticket : list) {
-+                reference.add(ticket, ticket.x, ticket.z, originDistance);
-+                test.setSource(ticket.x, ticket.z, originDistance);
-+            }
-+            test.propagateUpdates();
-+
-+            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<Ticket> list = new java.util.ArrayList<>();
-+            for (int i = 0; i < 10; ++i) {
-+                Ticket a = new Ticket();
-+                list.add(a);
-+                a.x = offX + ((i & 1) == 1 ? -i : i);
-+                a.z = offZ + ((i & 1) == 1 ? -i : i);
-+            }
-+            for (Ticket ticket : list) {
-+                reference.add(ticket, ticket.x, ticket.z, originDistance);
-+                test.setSource(ticket.x, ticket.z, originDistance);
-+            }
-+            test.propagateUpdates();
-+
-+            for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
-+                for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
-+                    test(dx, dz, reference, test);
-+                }
-+            }
-+
-+            // test ticket level decrease
-+
-+            for (Ticket ticket : list) {
-+                reference.update(ticket, ticket.x, ticket.z, originDistance/2);
-+                test.setSource(ticket.x, ticket.z, originDistance/2);
-+            }
-+            test.propagateUpdates();
-+
-+            for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
-+                for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
-+                    test(dx, dz, reference, test);
-+                }
-+            }
-+
-+            // test ticket level increase
-+
-+            for (Ticket ticket : list) {
-+                reference.update(ticket, ticket.x, ticket.z, originDistance*2);
-+                test.setSource(ticket.x, ticket.z, originDistance*2);
-+            }
-+            test.propagateUpdates();
-+
-+            for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
-+                for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
-+                    test(dx, dz, reference, test);
-+                }
-+            }
-+
-+            // test ticket remove
-+            for (int i = 0, len = list.size(); i < len; ++i) {
-+                if ((i & 3) != 0) {
-+                    continue;
-+                }
-+                Ticket ticket = list.get(i);
-+                reference.remove(ticket);
-+                test.removeSource(ticket.x, ticket.z);
-+            }
-+            test.propagateUpdates();
-+
-+            for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
-+                for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
-+                    test(dx, dz, reference, test);
-+                }
-+            }
-+        }
-+    }
-+     */
-+
-+    // this map is considered "stale" unless updates are propagated.
-+    protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
-+
-+    // this map is never stale
-+    protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
-+
-+    // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
-+    // propagating updates
-+    protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
-+
-+    @FunctionalInterface
-+    public static interface LevelChangeCallback {
-+
-+        /**
-+         * This can be called for intermediate updates. So do not rely on newLevel being close to or
-+         * the exact level that is expected after a full propagation has occured.
-+         */
-+        public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
-+
-+    }
-+
-+    protected final LevelChangeCallback changeCallback;
-+
-+    public Delayed8WayDistancePropagator2D() {
-+        this(null);
-+    }
-+
-+    public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
-+        this.changeCallback = changeCallback;
-+    }
-+
-+    public int getLevel(final long pos) {
-+        return this.levels.get(pos);
-+    }
-+
-+    public int getLevel(final int x, final int z) {
-+        return this.levels.get(CoordinateUtils.getChunkKey(x, z));
-+    }
-+
-+    public void setSource(final int x, final int z, final int level) {
-+        this.setSource(CoordinateUtils.getChunkKey(x, z), level);
-+    }
-+
-+    public void setSource(final long coordinate, final int level) {
-+        if ((level & 63) != level || level == 0) {
-+            throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
-+        }
-+
-+        final byte byteLevel = (byte)level;
-+        final byte oldLevel = this.sources.put(coordinate, byteLevel);
-+
-+        if (oldLevel == byteLevel) {
-+            return; // nothing to do
-+        }
-+
-+        // queue to update later
-+        this.updatedSources.add(coordinate);
-+    }
-+
-+    public void removeSource(final int x, final int z) {
-+        this.removeSource(CoordinateUtils.getChunkKey(x, z));
-+    }
-+
-+    public void removeSource(final long coordinate) {
-+        if (this.sources.remove(coordinate) != 0) {
-+            this.updatedSources.add(coordinate);
-+        }
-+    }
-+
-+    // queues used for BFS propagating levels
-+    protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
-+    {
-+        for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
-+            this.levelIncreaseWorkQueues[i] = new WorkQueue();
-+        }
-+    }
-+    protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
-+    {
-+        for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
-+            this.levelRemoveWorkQueues[i] = new WorkQueue();
-+        }
-+    }
-+    protected long levelIncreaseWorkQueueBitset;
-+    protected long levelRemoveWorkQueueBitset;
-+
-+    protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
-+        final WorkQueue queue = this.levelIncreaseWorkQueues[level];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelIncreaseWorkQueueBitset |= (1L << level);
-+    }
-+
-+    protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
-+        final WorkQueue queue = this.levelIncreaseWorkQueues[index];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelIncreaseWorkQueueBitset |= (1L << index);
-+    }
-+
-+    protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
-+        final WorkQueue queue = this.levelRemoveWorkQueues[level];
-+        queue.queuedCoordinates.enqueue(coordinate);
-+        queue.queuedLevels.enqueue(level);
-+
-+        this.levelRemoveWorkQueueBitset |= (1L << level);
-+    }
-+
-+    public boolean propagateUpdates() {
-+        if (this.updatedSources.isEmpty()) {
-+            return false;
-+        }
-+
-+        boolean ret = false;
-+
-+        for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
-+            final long coordinate = iterator.nextLong();
-+
-+            final byte currentLevel = this.levels.get(coordinate);
-+            final byte updatedSource = this.sources.get(coordinate);
-+
-+            if (currentLevel == updatedSource) {
-+                continue;
-+            }
-+            ret = true;
-+
-+            if (updatedSource > currentLevel) {
-+                // level increase
-+                this.addToIncreaseWorkQueue(coordinate, updatedSource);
-+            } else {
-+                // level decrease
-+                this.addToRemoveWorkQueue(coordinate, currentLevel);
-+                // if the current coordinate is a source, then the decrease propagation will detect that and queue
-+                // the source propagation
-+            }
-+        }
-+
-+        this.updatedSources.clear();
-+
-+        // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
-+        // make the removes remove less)
-+        this.propagateIncreases();
-+
-+        // now we propagate the decreases (which will then re-propagate clobbered sources)
-+        this.propagateDecreases();
-+
-+        return ret;
-+    }
-+
-+    protected void propagateIncreases() {
-+        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
-+             this.levelIncreaseWorkQueueBitset != 0L;
-+             this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
-+
-+            final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
-+            while (!queue.queuedLevels.isEmpty()) {
-+                final long coordinate = queue.queuedCoordinates.removeFirstLong();
-+                byte level = queue.queuedLevels.removeFirstByte();
-+
-+                final boolean neighbourCheck = level < 0;
-+
-+                final byte currentLevel;
-+                if (neighbourCheck) {
-+                    level = (byte)-level;
-+                    currentLevel = this.levels.get(coordinate);
-+                } else {
-+                    currentLevel = this.levels.putIfGreater(coordinate, level);
-+                }
-+
-+                if (neighbourCheck) {
-+                    // used when propagating from decrease to indicate that this level needs to check its neighbours
-+                    // this means the level at coordinate could be equal, but would still need neighbours checked
-+
-+                    if (currentLevel != level) {
-+                        // something caused the level to change, which means something propagated to it (which means
-+                        // us propagating here is redundant), or something removed the level (which means we
-+                        // cannot propagate further)
-+                        continue;
-+                    }
-+                } else if (currentLevel >= level) {
-+                    // something higher/equal propagated
-+                    continue;
-+                }
-+                if (this.changeCallback != null) {
-+                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
-+                }
-+
-+                if (level == 1) {
-+                    // can't propagate 0 to neighbours
-+                    continue;
-+                }
-+
-+                // propagate to neighbours
-+                final byte neighbourLevel = (byte)(level - 1);
-+                final int x = (int)coordinate;
-+                final int z = (int)(coordinate >>> 32);
-+
-+                for (int dx = -1; dx <= 1; ++dx) {
-+                    for (int dz = -1; dz <= 1; ++dz) {
-+                        if ((dx | dz) == 0) {
-+                            // already propagated to coordinate
-+                            continue;
-+                        }
-+
-+                        // sure we can check the neighbour level in the map right now and avoid a propagation,
-+                        // but then we would still have to recheck it when popping the value off of the queue!
-+                        // so just avoid the double lookup
-+                        final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
-+                        this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
-+                    }
-+                }
-+            }
-+        }
-+    }
-+
-+    protected void propagateDecreases() {
-+        for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
-+             this.levelRemoveWorkQueueBitset != 0L;
-+             this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
-+
-+            final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
-+            while (!queue.queuedLevels.isEmpty()) {
-+                final long coordinate = queue.queuedCoordinates.removeFirstLong();
-+                final byte level = queue.queuedLevels.removeFirstByte();
-+
-+                final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
-+                if (currentLevel == 0) {
-+                    // something else removed
-+                    continue;
-+                }
-+
-+                if (currentLevel > level) {
-+                    // something higher propagated here or we hit the propagation of another source
-+                    // in the second case we need to re-propagate because we could have just clobbered another source's
-+                    // propagation
-+                    this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
-+                    continue;
-+                }
-+
-+                if (this.changeCallback != null) {
-+                    this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
-+                }
-+
-+                final byte source = this.sources.get(coordinate);
-+                if (source != 0) {
-+                    // must re-propagate source later
-+                    this.addToIncreaseWorkQueue(coordinate, source);
-+                }
-+
-+                if (level == 0) {
-+                    // can't propagate -1 to neighbours
-+                    // we have to check neighbours for removing 1 just in case the neighbour is 2
-+                    continue;
-+                }
-+
-+                // propagate to neighbours
-+                final byte neighbourLevel = (byte)(level - 1);
-+                final int x = (int)coordinate;
-+                final int z = (int)(coordinate >>> 32);
-+
-+                for (int dx = -1; dx <= 1; ++dx) {
-+                    for (int dz = -1; dz <= 1; ++dz) {
-+                        if ((dx | dz) == 0) {
-+                            // already propagated to coordinate
-+                            continue;
-+                        }
-+
-+                        // sure we can check the neighbour level in the map right now and avoid a propagation,
-+                        // but then we would still have to recheck it when popping the value off of the queue!
-+                        // so just avoid the double lookup
-+                        final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
-+                        this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
-+                    }
-+                }
-+            }
-+        }
-+
-+        // propagate sources we clobbered in the process
-+        this.propagateIncreases();
-+    }
-+
-+    protected static final class LevelMap extends Long2ByteOpenHashMap {
-+        public LevelMap() {
-+            super();
-+        }
-+
-+        public LevelMap(final int expected, final float loadFactor) {
-+            super(expected, loadFactor);
-+        }
-+
-+        // copied from superclass
-+        private int find(final long k) {
-+            if (k == 0L) {
-+                return this.containsNullKey ? this.n : -(this.n + 1);
-+            } else {
-+                final long[] key = this.key;
-+                long curr;
-+                int pos;
-+                if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
-+                    return -(pos + 1);
-+                } else if (k == curr) {
-+                    return pos;
-+                } else {
-+                    while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
-+                        if (k == curr) {
-+                            return pos;
-+                        }
-+                    }
-+
-+                    return -(pos + 1);
-+                }
-+            }
-+        }
-+
-+        // copied from superclass
-+        private void insert(final int pos, final long k, final byte v) {
-+            if (pos == this.n) {
-+                this.containsNullKey = true;
-+            }
-+
-+            this.key[pos] = k;
-+            this.value[pos] = v;
-+            if (this.size++ >= this.maxFill) {
-+                this.rehash(HashCommon.arraySize(this.size + 1, this.f));
-+            }
-+        }
-+
-+        // copied from superclass
-+        public byte putIfGreater(final long key, final byte value) {
-+            final int pos = this.find(key);
-+            if (pos < 0) {
-+                if (this.defRetValue < value) {
-+                    this.insert(-pos - 1, key, value);
-+                }
-+                return this.defRetValue;
-+            } else {
-+                final byte curr = this.value[pos];
-+                if (value > curr) {
-+                    this.value[pos] = value;
-+                    return curr;
-+                }
-+                return curr;
-+            }
-+        }
-+
-+        // copied from superclass
-+        private void removeEntry(final int pos) {
-+            --this.size;
-+            this.shiftKeys(pos);
-+            if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
-+                this.rehash(this.n / 2);
-+            }
-+        }
-+
-+        // copied from superclass
-+        private void removeNullEntry() {
-+            this.containsNullKey = false;
-+            --this.size;
-+            if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
-+                this.rehash(this.n / 2);
-+            }
-+        }
-+
-+        // copied from superclass
-+        public byte removeIfGreaterOrEqual(final long key, final byte value) {
-+            if (key == 0L) {
-+                if (!this.containsNullKey) {
-+                    return this.defRetValue;
-+                }
-+                final byte current = this.value[this.n];
-+                if (value >= current) {
-+                    this.removeNullEntry();
-+                    return current;
-+                }
-+                return current;
-+            } else {
-+                long[] keys = this.key;
-+                byte[] values = this.value;
-+                long curr;
-+                int pos;
-+                if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
-+                    return this.defRetValue;
-+                } else if (key == curr) {
-+                    final byte current = values[pos];
-+                    if (value >= current) {
-+                        this.removeEntry(pos);
-+                        return current;
-+                    }
-+                    return current;
-+                } else {
-+                    while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
-+                        if (key == curr) {
-+                            final byte current = values[pos];
-+                            if (value >= current) {
-+                                this.removeEntry(pos);
-+                                return current;
-+                            }
-+                            return current;
-+                        }
-+                    }
-+
-+                    return this.defRetValue;
-+                }
-+            }
-+        }
-+    }
-+
-+    protected static final class WorkQueue {
-+
-+        public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
-+        public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
-+
-+    }
-+
-+    protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
-+
-+        /**
-+         * Assumes non-empty. If empty, undefined behaviour.
-+         */
-+        public long removeFirstLong() {
-+            // copied from superclass
-+            long t = this.array[this.start];
-+            if (++this.start == this.length) {
-+                this.start = 0;
-+            }
-+
-+            return t;
-+        }
-+    }
-+
-+    protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
-+
-+        /**
-+         * Assumes non-empty. If empty, undefined behaviour.
-+         */
-+        public byte removeFirstByte() {
-+            // copied from superclass
-+            byte t = this.array[this.start];
-+            if (++this.start == this.length) {
-+                this.start = 0;
-+            }
-+
-+            return t;
-+        }
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+import ca.spottedleaf.moonrise.common.list.ReferenceList;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
-+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class NearbyPlayers {
-+
-+    public static enum NearbyMapType {
-+        GENERAL,
-+        GENERAL_SMALL,
-+        GENERAL_REALLY_SMALL,
-+        TICK_VIEW_DISTANCE,
-+        VIEW_DISTANCE,
-+        SPAWN_RANGE,
-+    }
-+
-+    private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
-+    public static final int TOTAL_MAP_TYPES = MAP_TYPES.length;
-+
-+    private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1;
-+    private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
-+    private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
-+
-+    public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
-+    public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
-+    public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
-+
-+    private final ServerLevel world;
-+    private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
-+    private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
-+
-+    public NearbyPlayers(final ServerLevel world) {
-+        this.world = world;
-+    }
-+
-+    public void addPlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
-+        if (this.players.putIfAbsent(player, newTrackers) != null) {
-+            throw new IllegalStateException("Already have player " + player);
-+        }
-+
-+        final ChunkPos chunk = player.chunkPosition();
-+
-+        for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
-+            // use 0 for default, will be updated by tickPlayer
-+            (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0);
-+        }
-+
-+        // update view distances
-+        this.tickPlayer(player);
-+    }
-+
-+    public void removePlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] players = this.players.remove(player);
-+        if (players == null) {
-+            return; // May be called during teleportation before the player is actually placed
-+        }
-+
-+        for (final TrackedPlayer tracker : players) {
-+            tracker.remove();
-+        }
-+    }
-+
-+    public void tickPlayer(final ServerPlayer player) {
-+        final TrackedPlayer[] players = this.players.get(player);
-+        if (players == null) {
-+            throw new IllegalStateException("Don't have player " + player);
-+        }
-+
-+        final ChunkPos chunk = player.chunkPosition();
-+
-+        players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
-+        players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
-+        players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
-+        players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
-+        players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
-+    }
-+
-+    public TrackedChunk getChunk(final ChunkPos pos) {
-+        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+    }
-+
-+    public TrackedChunk getChunk(final BlockPos pos) {
-+        return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
-+        final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
-+
-+        return chunk == null ? null : chunk.players[type.ordinal()];
-+    }
-+
-+    public static final class TrackedChunk {
-+
-+        private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
-+
-+        private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
-+        private int nonEmptyLists;
-+        private long updateCount;
-+
-+        public boolean isEmpty() {
-+            return this.nonEmptyLists == 0;
-+        }
-+
-+        public long getUpdateCount() {
-+            return this.updateCount;
-+        }
-+
-+        public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
-+            return this.players[type.ordinal()];
-+        }
-+
-+        public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
-+            ++this.updateCount;
-+
-+            final int idx = type.ordinal();
-+            final ReferenceList<ServerPlayer> list = this.players[idx];
-+            if (list == null) {
-+                ++this.nonEmptyLists;
-+                (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY, 0)).add(player);
-+                return;
-+            }
-+
-+            if (!list.add(player)) {
-+                throw new IllegalStateException("Already contains player " + player);
-+            }
-+        }
-+
-+        public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
-+            ++this.updateCount;
-+
-+            final int idx = type.ordinal();
-+            final ReferenceList<ServerPlayer> list = this.players[idx];
-+            if (list == null) {
-+                throw new IllegalStateException("Does not contain player " + player);
-+            }
-+
-+            if (!list.remove(player)) {
-+                throw new IllegalStateException("Does not contain player " + player);
-+            }
-+
-+            if (list.size() == 0) {
-+                this.players[idx] = null;
-+                --this.nonEmptyLists;
-+            }
-+        }
-+    }
-+
-+    private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
-+
-+        private final NearbyMapType type;
-+
-+        public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
-+            super(player);
-+            this.type = type;
-+        }
-+
-+        @Override
-+        protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+            NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
-+                return new TrackedChunk();
-+            }).addPlayer(parameter, this.type);
-+        }
-+
-+        @Override
-+        protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+            final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+            final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
-+            if (chunk == null) {
-+                throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
-+            }
-+
-+            chunk.removePlayer(parameter, this.type);
-+
-+            if (chunk.isEmpty()) {
-+                NearbyPlayers.this.byChunk.remove(chunkKey);
-+            }
-+        }
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
-+
-+public abstract class SingleUserAreaMap<T> {
-+
-+    private static final int NOT_SET = Integer.MIN_VALUE;
-+
-+    private final T parameter;
-+    private int lastChunkX = NOT_SET;
-+    private int lastChunkZ = NOT_SET;
-+    private int distance = NOT_SET;
-+
-+    public SingleUserAreaMap(final T parameter) {
-+        this.parameter = parameter;
-+    }
-+
-+    /* math sign function except 0 returns 1 */
-+    protected static int sign(int val) {
-+        return 1 | (val >> (Integer.SIZE - 1));
-+    }
-+
-+    protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+    protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+    private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+        final int maxX = chunkX + distance;
-+        final int maxZ = chunkZ + distance;
-+
-+        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+                this.addCallback(parameter, cx, cz);
-+            }
-+        }
-+    }
-+
-+    private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+        final int maxX = chunkX + distance;
-+        final int maxZ = chunkZ + distance;
-+
-+        for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+            for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+                this.removeCallback(parameter, cx, cz);
-+            }
-+        }
-+    }
-+
-+    public final boolean add(final int chunkX, final int chunkZ, final int distance) {
-+        if (distance < 0) {
-+            throw new IllegalArgumentException(Integer.toString(distance));
-+        }
-+        if (this.lastChunkX != NOT_SET) {
-+            return false;
-+        }
-+        this.lastChunkX = chunkX;
-+        this.lastChunkZ = chunkZ;
-+        this.distance = distance;
-+
-+        this.addToNew(this.parameter, chunkX, chunkZ, distance);
-+
-+        return true;
-+    }
-+
-+    public final boolean update(final int toX, final int toZ, final int newViewDistance) {
-+        if (newViewDistance < 0) {
-+            throw new IllegalArgumentException(Integer.toString(newViewDistance));
-+        }
-+        final int fromX = this.lastChunkX;
-+        final int fromZ = this.lastChunkZ;
-+        final int oldViewDistance = this.distance;
-+        if (fromX == NOT_SET) {
-+            return false;
-+        }
-+
-+        this.lastChunkX = toX;
-+        this.lastChunkZ = toZ;
-+        this.distance = newViewDistance;
-+
-+        final T parameter = this.parameter;
-+
-+
-+        final int dx = toX - fromX;
-+        final int dz = toZ - fromZ;
-+
-+        final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
-+
-+        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+            // teleported
-+            this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
-+            this.addToNew(parameter, toX, toZ, newViewDistance);
-+            return true;
-+        }
-+
-+        if (oldViewDistance != newViewDistance) {
-+            // remove loop
-+
-+            final int oldMinX = fromX - oldViewDistance;
-+            final int oldMinZ = fromZ - oldViewDistance;
-+            final int oldMaxX = fromX + oldViewDistance;
-+            final int oldMaxZ = fromZ + oldViewDistance;
-+            for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
-+                for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
-+
-+                    // only remove if we're outside the new view distance...
-+                    if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
-+                        this.removeCallback(parameter, currX, currZ);
-+                    }
-+                }
-+            }
-+
-+            // add loop
-+
-+            final int newMinX = toX - newViewDistance;
-+            final int newMinZ = toZ - newViewDistance;
-+            final int newMaxX = toX + newViewDistance;
-+            final int newMaxZ = toZ + newViewDistance;
-+            for (int currX = newMinX; currX <= newMaxX; ++currX) {
-+                for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
-+
-+                    // only add if we're outside the old view distance...
-+                    if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
-+                        this.addCallback(parameter, currX, currZ);
-+                    }
-+                }
-+            }
-+
-+            return true;
-+        }
-+
-+        // x axis is width
-+        // z axis is height
-+        // right refers to the x axis of where we moved
-+        // top refers to the z axis of where we moved
-+
-+        // same view distance
-+
-+        // used for relative positioning
-+        final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
-+        final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
-+
-+        // The area excluded by overlapping the two view distance squares creates four rectangles:
-+        // Two on the left, and two on the right. The ones on the left we consider the "removed" section
-+        // and on the right the "added" section.
-+        // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
-+        // exclusive to the regions they surround.
-+
-+        // 4 points of the rectangle
-+        int maxX; // exclusive
-+        int minX; // inclusive
-+        int maxZ; // exclusive
-+        int minZ; // inclusive
-+
-+        if (dx != 0) {
-+            // handle right addition
-+
-+            maxX = toX + (oldViewDistance * right) + right; // exclusive
-+            minX = fromX + (oldViewDistance * right) + right; // inclusive
-+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dz != 0) {
-+            // handle up addition
-+
-+            maxX = toX + (oldViewDistance * right) + right; // exclusive
-+            minX = toX - (oldViewDistance * right); // inclusive
-+            maxZ = toZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = fromZ + (oldViewDistance * up) + up; // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.addCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dx != 0) {
-+            // handle left removal
-+
-+            maxX = toX - (oldViewDistance * right); // exclusive
-+            minX = fromX - (oldViewDistance * right); // inclusive
-+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+            minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        if (dz != 0) {
-+            // handle down removal
-+
-+            maxX = fromX + (oldViewDistance * right) + right; // exclusive
-+            minX = fromX - (oldViewDistance * right); // inclusive
-+            maxZ = toZ - (oldViewDistance * up); // exclusive
-+            minZ = fromZ - (oldViewDistance * up); // inclusive
-+
-+            for (int currX = minX; currX != maxX; currX += right) {
-+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+                    this.removeCallback(parameter, currX, currZ);
-+                }
-+            }
-+        }
-+
-+        return true;
-+    }
-+
-+    public final boolean remove() {
-+        final int chunkX = this.lastChunkX;
-+        final int chunkZ = this.lastChunkZ;
-+        final int distance = this.distance;
-+        if (chunkX == NOT_SET) {
-+            return false;
-+        }
-+
-+        this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
-+
-+        this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
-+
-+        return true;
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.set;
-+
-+import java.util.Collection;
-+
-+public final class OptimizedSmallEnumSet<E extends Enum<E>> {
-+
-+    private final Class<E> enumClass;
-+    private long backingSet;
-+
-+    public OptimizedSmallEnumSet(final Class<E> clazz) {
-+        if (clazz == null) {
-+            throw new IllegalArgumentException("Null class");
-+        }
-+        if (!clazz.isEnum()) {
-+            throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
-+        }
-+        this.enumClass = clazz;
-+    }
-+
-+    public boolean addUnchecked(final E element) {
-+        final int ordinal = element.ordinal();
-+        final long key = 1L << ordinal;
-+
-+        final long prev = this.backingSet;
-+        this.backingSet = prev | key;
-+
-+        return (prev & key) == 0;
-+    }
-+
-+    public boolean removeUnchecked(final E element) {
-+        final int ordinal = element.ordinal();
-+        final long key = 1L << ordinal;
-+
-+        final long prev = this.backingSet;
-+        this.backingSet = prev & ~key;
-+
-+        return (prev & key) != 0;
-+    }
-+
-+    public void clear() {
-+        this.backingSet = 0L;
-+    }
-+
-+    public int size() {
-+        return Long.bitCount(this.backingSet);
-+    }
-+
-+    public void addAllUnchecked(final Collection<E> enums) {
-+        for (final E element : enums) {
-+            if (element == null) {
-+                throw new NullPointerException("Null element");
-+            }
-+            this.backingSet |= (1L << element.ordinal());
-+        }
-+    }
-+
-+    public long getBackingSet() {
-+        return this.backingSet;
-+    }
-+
-+    public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
-+        return (other.backingSet & this.backingSet) != 0;
-+    }
-+
-+    public boolean hasElement(final E element) {
-+        return (this.backingSet & (1L << element.ordinal())) != 0;
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.phys.Vec3;
-+
-+public final class CoordinateUtils {
-+
-+    // the chunk keys are compatible with vanilla
-+
-+    public static long getChunkKey(final BlockPos pos) {
-+        return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
-+    }
-+
-+    public static long getChunkKey(final Entity entity) {
-+        return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
-+    }
-+
-+    public static long getChunkKey(final ChunkPos pos) {
-+        return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
-+    }
-+
-+    public static long getChunkKey(final SectionPos pos) {
-+        return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
-+    }
-+
-+    public static long getChunkKey(final int x, final int z) {
-+        return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+    }
-+
-+    public static int getChunkX(final long chunkKey) {
-+        return (int)chunkKey;
-+    }
-+
-+    public static int getChunkZ(final long chunkKey) {
-+        return (int)(chunkKey >>> 32);
-+    }
-+
-+    public static int getChunkCoordinate(final double blockCoordinate) {
-+        return Mth.floor(blockCoordinate) >> 4;
-+    }
-+
-+    // the section keys are compatible with vanilla's
-+
-+    static final int SECTION_X_BITS = 22;
-+    static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
-+    static final int SECTION_Y_BITS = 20;
-+    static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
-+    static final int SECTION_Z_BITS = 22;
-+    static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
-+    // format is y,z,x (in order of LSB to MSB)
-+    static final int SECTION_Y_SHIFT = 0;
-+    static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
-+    static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
-+    static final int SECTION_TO_BLOCK_SHIFT = 4;
-+
-+    public static long getChunkSectionKey(final int x, final int y, final int z) {
-+        return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+                | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+                | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+    }
-+
-+    public static long getChunkSectionKey(final SectionPos pos) {
-+        return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
-+                | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+                | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+    }
-+
-+    public static long getChunkSectionKey(final ChunkPos pos, final int y) {
-+        return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+                | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+                | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+    }
-+
-+    public static long getChunkSectionKey(final BlockPos pos) {
-+        return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+                ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+                (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+    }
-+
-+    public static long getChunkSectionKey(final Entity entity) {
-+        return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+                ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+                ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+    }
-+
-+    public static int getChunkSectionX(final long key) {
-+        return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
-+    }
-+
-+    public static int getChunkSectionY(final long key) {
-+        return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
-+    }
-+
-+    public static int getChunkSectionZ(final long key) {
-+        return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
-+    }
-+
-+    public static int getBlockX(final Vec3 pos) {
-+        return Mth.floor(pos.x);
-+    }
-+
-+    public static int getBlockY(final Vec3 pos) {
-+        return Mth.floor(pos.y);
-+    }
-+
-+    public static int getBlockZ(final Vec3 pos) {
-+        return Mth.floor(pos.z);
-+    }
-+
-+    public static int getChunkX(final Vec3 pos) {
-+        return Mth.floor(pos.x) >> 4;
-+    }
-+
-+    public static int getChunkY(final Vec3 pos) {
-+        return Mth.floor(pos.y) >> 4;
-+    }
-+
-+    public static int getChunkZ(final Vec3 pos) {
-+        return Mth.floor(pos.z) >> 4;
-+    }
-+
-+    private CoordinateUtils() {
-+        throw new RuntimeException();
-+    }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import java.util.Objects;
-+
-+public final class FlatBitsetUtil {
-+
-+    private static final int LOG2_LONG = 6;
-+    private static final long ALL_SET = -1L;
-+    private static final int BITS_PER_LONG = Long.SIZE;
-+
-+    // from inclusive
-+    // to exclusive
-+    public static int firstSet(final long[] bitset, final int from, final int to) {
-+        if ((from | to | (to - from)) < 0) {
-+            throw new IndexOutOfBoundsException();
-+        }
-+
-+        int bitsetIdx = from >>> LOG2_LONG;
-+        int bitIdx = from & ~(BITS_PER_LONG - 1);
-+
-+        long tmp = bitset[bitsetIdx] & (ALL_SET << from);
-+        for (;;) {
-+            if (tmp != 0L) {
-+                final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
-+                return ret >= to ? -1 : ret;
-+            }
-+
-+            bitIdx += BITS_PER_LONG;
-+
-+            if (bitIdx >= to) {
-+                return -1;
-+            }
-+
-+            tmp = bitset[++bitsetIdx];
-+        }
-+    }
-+
-+    // from inclusive
-+    // to exclusive
-+    public static int firstClear(final long[] bitset, final int from, final int to) {
-+        if ((from | to | (to - from)) < 0) {
-+            throw new IndexOutOfBoundsException();
-+        }
-+        // like firstSet, but invert the bitset
-+
-+        int bitsetIdx = from >>> LOG2_LONG;
-+        int bitIdx = from & ~(BITS_PER_LONG - 1);
-+
-+        long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
-+        for (;;) {
-+            if (tmp != 0L) {
-+                final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
-+                return ret >= to ? -1 : ret;
-+            }
-+
-+            bitIdx += BITS_PER_LONG;
-+
-+            if (bitIdx >= to) {
-+                return -1;
-+            }
-+
-+            tmp = ~bitset[++bitsetIdx];
-+        }
-+    }
-+
-+    // from inclusive
-+    // to exclusive
-+    public static void clearRange(final long[] bitset, final int from, int to) {
-+        if ((from | to | (to - from)) < 0) {
-+            throw new IndexOutOfBoundsException();
-+        }
-+
-+        if (from == to) {
-+            return;
-+        }
-+
-+        --to;
-+
-+        final int fromBitsetIdx = from >>> LOG2_LONG;
-+        final int toBitsetIdx = to >>> LOG2_LONG;
-+
-+        final long keepFirst = ~(ALL_SET << from);
-+        final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
-+
-+        Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
-+
-+        if (fromBitsetIdx == toBitsetIdx) {
-+            // special case: need to keep both first and last
-+            bitset[fromBitsetIdx] &= (keepFirst | keepLast);
-+        } else {
-+            bitset[fromBitsetIdx] &= keepFirst;
-+
-+            for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
-+                bitset[i] = 0L;
-+            }
-+
-+            bitset[toBitsetIdx] &= keepLast;
-+        }
-+    }
-+
-+    // from inclusive
-+    // to exclusive
-+    public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
-+        return firstClear(bitset, from, to) == -1;
-+    }
-+
-+
-+    private FlatBitsetUtil() {}
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import com.google.gson.JsonElement;
-+import com.google.gson.internal.Streams;
-+import com.google.gson.stream.JsonWriter;
-+import java.io.File;
-+import java.io.FileOutputStream;
-+import java.io.IOException;
-+import java.io.PrintStream;
-+import java.io.StringWriter;
-+import java.nio.charset.StandardCharsets;
-+
-+public final class JsonUtil {
-+
-+    public static void writeJson(final JsonElement element, final File file) throws IOException {
-+        final StringWriter stringWriter = new StringWriter();
-+        final JsonWriter jsonWriter = new JsonWriter(stringWriter);
-+        jsonWriter.setIndent(" ");
-+        jsonWriter.setLenient(false);
-+        Streams.write(element, jsonWriter);
-+
-+        final String jsonString = stringWriter.toString();
-+
-+        final File parent = file.getParentFile();
-+        if (parent != null) {
-+            parent.mkdirs();
-+        }
-+        file.createNewFile();
-+        try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
-+            out.print(jsonString);
-+        }
-+    }
-+
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+public final class MixinWorkarounds {
-+
-+    // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
-+    public static long[] clone(final long[] values) {
-+        return values.clone();
-+    }
-+
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+
-+public final class MoonriseCommon {
-+
-+    private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
-+
-+    // Paper start
-+    public static PrioritisedThreadPool WORKER_POOL;
-+    public static int WORKER_THREADS;
-+    public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
-+        // Paper end
-+        int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
-+        if (defaultWorkerThreads <= 4) {
-+            defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
-+        } else {
-+            defaultWorkerThreads = defaultWorkerThreads / 2;
-+        }
-+        defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
-+
-+        int workerThreads = chunkSystem.workerThreads;
-+
-+        if (workerThreads <= 0) {
-+            workerThreads = defaultWorkerThreads;
-+        }
-+
-+        WORKER_POOL = new PrioritisedThreadPool(
-+                "Paper Worker Pool", workerThreads,
-+                (final Thread thread, final Integer id) -> {
-+                    thread.setName("Paper Common Worker #" + id.intValue());
-+                    thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
-+                        @Override
-+                        public void uncaughtException(final Thread thread, final Throwable throwable) {
-+                            LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
-+                        }
-+                    });
-+                }, (long)(20.0e6)); // 20ms
-+        WORKER_THREADS = workerThreads;
-+    }
-+
-+    private MoonriseCommon() {}
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+public final class MoonriseConstants {
-+
-+    public static final int MAX_VIEW_DISTANCE = 32;
-+
-+    private MoonriseConstants() {}
-+
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+    // min, max are inclusive
-+
-+    public static int getMaxSection(final LevelHeightAccessor world) {
-+        return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+    }
-+
-+    public static int getMinSection(final LevelHeightAccessor world) {
-+        return world.getMinSection();
-+    }
-+
-+    public static int getMaxLightSection(final LevelHeightAccessor world) {
-+        return getMaxSection(world) + 1;
-+    }
-+
-+    public static int getMinLightSection(final LevelHeightAccessor world) {
-+        return getMinSection(world) - 1;
-+    }
-+
-+
-+
-+    public static int getTotalSections(final LevelHeightAccessor world) {
-+        return getMaxSection(world) - getMinSection(world) + 1;
-+    }
-+
-+    public static int getTotalLightSections(final LevelHeightAccessor world) {
-+        return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+    }
-+
-+    public static int getMinBlockY(final LevelHeightAccessor world) {
-+        return getMinSection(world) << 4;
-+    }
-+
-+    public static int getMaxBlockY(final LevelHeightAccessor world) {
-+        return (getMaxSection(world) << 4) | 15;
-+    }
-+
-+    public static String getWorldName(final Level world) {
-+        if (world == null) {
-+            return "null world";
-+        }
-+        return world.getWorld().getName();
-+    }
-+
-+    private WorldUtil() {
-+        throw new RuntimeException();
 +    }
-+}
+ 
++    public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
++                .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
+     }
+ 
+     public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        );
++        if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
++            chunk.postProcessGeneration();
++        }
++        ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
++        ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
+     }
+ 
+     public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        );
+     }
+ 
+     public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        );
+     }
+ 
+     public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+-
++        ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
++                ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
++        );
+     }
+ 
+     public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+-        return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
++        return null;
+     }
+ 
+     public static int getSendViewDistance(final ServerPlayer player) {
+-        return getLoadViewDistance(player);
++        return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
+     }
+ 
+     public static int getLoadViewDistance(final ServerPlayer player) {
+-        final ServerLevel level = player.serverLevel();
+-        if (level == null) {
+-            return org.bukkit.Bukkit.getViewDistance();
+-        }
+-        return level.chunkSource.chunkMap.getPlayerViewDistance(player);
++        return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
+     }
+ 
+     public static int getTickViewDistance(final ServerPlayer player) {
+-        final ServerLevel level = player.serverLevel();
+-        if (level == null) {
+-            return org.bukkit.Bukkit.getSimulationDistance();
+-        }
+-        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
++        return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
++    }
++
++    public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
++    }
++
++    public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
++    }
++
++    public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
++        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
+     }
+ 
+     private ChunkSystem() {}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -0,0 +0,0 @@ public class TickThread extends Thread {
+     }
+ 
+     public static boolean isTickThread() {
+-        return org.bukkit.Bukkit.isPrimaryThread(); // Paper
++        return Thread.currentThread() instanceof TickThread;
+     }
+ 
+     public static boolean isShutdownThread() {
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -3412,174 +401,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public BlockState moonrise$getBlock(final int x, final int y, final int z);
 +
 +}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
-@@ -0,0 +0,0 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system;
-+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
-+import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import org.slf4j.Logger;
-+import java.util.List;
-+import java.util.function.Consumer;
-+
-+public final class ChunkSystem {
-+
-+    private static final Logger LOGGER = LogUtils.getLogger();
-+
-+    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
-+        scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
-+    }
-+
-+    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
-+    }
-+
-+    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
-+                                         final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
-+                                         final Consumer<ChunkAccess> onComplete) {
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
-+    }
-+
-+    // Paper - rewrite chunk system
-+    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-+                                         final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+    }
-+
-+    public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
-+                                            final FullChunkStatus toStatus, final boolean addTicket,
-+                                            final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
-+        ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+    }
-+
-+    public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
-+    }
-+
-+    public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
-+    }
-+
-+    public static int getVisibleChunkHolderCount(final ServerLevel level) {
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
-+    }
-+
-+    public static int getUpdatingChunkHolderCount(final ServerLevel level) {
-+        return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
-+    }
-+
-+    public static boolean hasAnyChunkHolders(final ServerLevel level) {
-+        return getUpdatingChunkHolderCount(level) != 0;
-+    }
-+
-+    public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd(level, entity);
-+    }
-+
-+    public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(level, holder);
-+    }
-+
-+    public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(level, holder);
-+    }
-+
-+    public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
-+            .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
-+    }
-+
-+    public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, holder);
-+        chunk.loadCallback(); // Paper
-+    }
-+
-+    public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(chunk, holder);
-+        chunk.unloadCallback(); // Paper
-+    }
-+
-+    public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+        ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
-+            .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
-+    }
-+
-+    public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, holder);
-+        if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
-+            chunk.postProcessGeneration();
-+        }
-+        ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
-+        ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
-+    }
-+
-+    public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(chunk, holder);
-+    }
-+
-+    public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, holder);
-+    }
-+
-+    public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+        // TODO move hook
-+        io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(chunk, holder);
-+    }
-+
-+    public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
-+        return null;
-+    }
-+
-+    public static int getSendViewDistance(final ServerPlayer player) {
-+        return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
-+    }
-+
-+    public static int getLoadViewDistance(final ServerPlayer player) {
-+        return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
-+    }
-+
-+    public static int getTickViewDistance(final ServerPlayer player) {
-+        return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
-+    }
-+
-+    public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
-+    }
-+
-+    public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
-+    }
-+
-+    public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
-+        ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
-+    }
-+
-+    private ChunkSystem() {}
-+}
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -3765,6 +586,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import net.minecraft.nbt.CompoundTag;
@@ -4052,7 +874,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @return The priroity to use with blocking I/O on the current thread.
 +     */
 +    public static Priority getIOBlockingPriorityForCurrentThread() {
-+        if (io.papermc.paper.util.TickThread.isTickThread()) {
++        if (TickThread.isTickThread()) {
 +            return Priority.BLOCKING;
 +        }
 +        return Priority.HIGHEST;
@@ -5208,11 +2030,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package ca.spottedleaf.moonrise.patches.chunk_system.level;
 +
 +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
 +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
 +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
 +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
 +import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerChunkCache;
 +import net.minecraft.world.level.chunk.ChunkAccess;
 +import net.minecraft.world.level.chunk.status.ChunkStatus;
 +import java.util.List;
@@ -5257,6 +2081,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void moonrise$setLastMidTickFailure(final long time);
 +
 +    public NearbyPlayers moonrise$getNearbyPlayers();
++
++    public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks();
++
++    public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks();
++
++    public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks();
 +}
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
 new file mode 100644
@@ -5345,10 +2175,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
 +
++import net.minecraft.server.level.ServerChunkCache;
++
 +public interface ChunkSystemLevelChunk {
 +
 +    public boolean moonrise$isPostProcessingDone();
 +
++    public ServerChunkCache.ChunkAndHolder moonrise$getChunkAndHolder();
++
++    public void moonrise$setChunkAndHolder(final ServerChunkCache.ChunkAndHolder holder);
++
 +}
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
 new file mode 100644
@@ -6274,6 +3110,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    protected abstract void entityEndTicking(final Entity entity);
 +
++    protected abstract boolean screenEntity(final Entity entity);
++
 +    private static Entity maskNonAccessible(final Entity entity) {
 +        if (entity == null) {
 +            return null;
@@ -6595,6 +3433,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return false;
 +        }
 +
++        if (!this.screenEntity(entity)) {
++            return false;
++        }
++
 +        Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity);
 +        if (currentlyMapped != null) {
 +            LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity);
@@ -7363,6 +4205,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    }
 +
++    @Override
++    protected boolean screenEntity(final Entity entity) {
++        return true;
++    }
++
 +    public void markTicking(final long pos) {
 +        if (this.tickingChunks.add(pos)) {
 +            final int chunkX = CoordinateUtils.getChunkX(pos);
@@ -7474,6 +4321,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    }
 +
++    @Override
++    protected boolean screenEntity(final Entity entity) {
++        return true;
++    }
++
 +    protected static final class DefaultLevelCallback implements LevelCallback<Entity> {
 +
 +        @Override
@@ -7507,6 +4359,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
 +
 +import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.util.TickThread;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
@@ -7520,8 +4374,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
 +
 +    private final ServerLevel serverWorld;
-+    public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY, 0); // Moonrise - entity tracker
-+    public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY, 0); // Moonrise - entity tracker
++    public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
++    public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
 +
 +    public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
 +        super(world, worldCallback);
@@ -7540,12 +4394,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
++        TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
 +    }
 +
 +    @Override
 +    protected void checkThread(final Entity entity, final String reason) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(entity, reason);
++        TickThread.ensureTickThread(entity, reason);
 +    }
 +
 +    @Override
@@ -7609,6 +4463,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    protected void entityEndTicking(final Entity entity) {
 +
 +    }
++
++    @Override
++    protected boolean screenEntity(final Entity entity) {
++        return ChunkSystem.screenEntity(this.serverWorld, entity);
++    }
 +}
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
 new file mode 100644
@@ -7660,6 +4519,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
 +
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
 +import com.mojang.serialization.Codec;
 +import com.mojang.serialization.DataResult;
@@ -7707,7 +4567,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public void load() {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
++        TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
 +        if (this.loaded) {
 +            return;
 +        }
@@ -7929,7 +4789,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter;
 +import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
@@ -8104,7 +4964,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public void addPlayer(final ServerPlayer player) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
++        TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
 +        if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
 +            return;
 +        }
@@ -8129,7 +4989,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public void removePlayer(final ServerPlayer player) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
++        TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
 +        if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
 +            return;
 +        }
@@ -8223,7 +5083,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public void tick() {
-+        io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick player chunk loader async");
++        TickThread.ensureTickThread("Cannot tick player chunk loader async");
 +        long currTime = System.nanoTime();
 +        for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
 +            final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
@@ -8551,7 +5411,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        void updateQueues(final long time) {
-+            io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
++            TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
 +            if (this.removed) {
 +                throw new IllegalStateException("Ticking removed player chunk loader");
 +            }
@@ -8736,7 +5596,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
 +                final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
 +                final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ);
-+                if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
++                if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
 +                    // nothing to do
 +                    // the target chunk may not be owned by this region, but this should be resolved in the future
 +                    break;
@@ -8763,7 +5623,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        void add() {
-+            io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
++            TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
 +            if (this.removed) {
 +                throw new IllegalStateException("Adding removed player chunk loader");
 +            }
@@ -8819,7 +5679,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        void update() {
-+            io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
++            TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
 +            if (this.removed) {
 +                throw new IllegalStateException("Updating removed player chunk loader");
 +            }
@@ -8971,7 +5831,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        void remove() {
-+            io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
++            TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
 +            if (this.removed) {
 +                throw new IllegalStateException("Removing removed player chunk loader");
 +            }
@@ -9162,8 +6022,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
@@ -9341,7 +6202,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public void close(final boolean save, final boolean halt) {
-+        io.papermc.paper.util.TickThread.ensureTickThread("Closing world off-main");
++        TickThread.ensureTickThread("Closing world off-main");
 +        if (halt) {
 +            LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
 +            if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
@@ -10017,7 +6878,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
++        TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
 +        ChunkEntitySlices ret;
 +
 +        NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
@@ -10098,7 +6959,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
++        TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
 +        PoiChunk ret;
 +
 +        NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
@@ -10166,7 +7027,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        if (changedFullStatus.isEmpty()) {
 +            return;
 +        }
-+        if (!io.papermc.paper.util.TickThread.isTickThread()) {
++        if (!TickThread.isTickThread()) {
 +            this.taskScheduler.scheduleChunkTask(() -> {
 +                final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
 +                for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -10193,7 +7054,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    // note: never call while inside the chunk system, this will absolutely break everything
 +    public void processUnloads() {
-+        io.papermc.paper.util.TickThread.ensureTickThread("Cannot unload chunks off-main");
++        TickThread.ensureTickThread("Cannot unload chunks off-main");
 +
 +        if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
 +            throw new IllegalStateException("Cannot unload chunks recursively");
@@ -10472,7 +7333,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        List<NewChunkHolder> changedFullStatus = null;
 +
-+        final boolean isTickThread = io.papermc.paper.util.TickThread.isTickThread();
++        final boolean isTickThread = TickThread.isTickThread();
 +
 +        boolean ret = false;
 +        final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
@@ -10598,6 +7459,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
 +import ca.spottedleaf.moonrise.common.util.JsonUtil;
 +import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
@@ -10955,7 +7817,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public boolean executeMainThreadTask() {
-+        io.papermc.paper.util.TickThread.ensureTickThread("Cannot execute main thread task off-main");
++        TickThread.ensureTickThread("Cannot execute main thread task off-main");
 +        return this.mainThreadExecutor.executeTask();
 +    }
 +
@@ -10974,7 +7836,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus,
 +                                     final boolean addTicket, final PrioritisedExecutor.Priority priority,
 +                                     final Consumer<LevelChunk> onComplete) {
-+        if (!io.papermc.paper.util.TickThread.isTickThread()) {
++        if (!TickThread.isTickThread()) {
 +            this.scheduleChunkTask(chunkX, chunkZ, () -> {
 +                ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 +            }, priority);
@@ -11165,7 +8027,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
 +                                  final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+        if (!io.papermc.paper.util.TickThread.isTickThread()) {
++        if (!TickThread.isTickThread()) {
 +            this.scheduleChunkTask(chunkX, chunkZ, () -> {
 +                ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
 +            }, priority);
@@ -11640,8 +8502,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
 +import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.TickThread;
 +import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
 +import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures;
 +import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
 +import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
@@ -11705,7 +8568,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private CompoundTag pendingEntityChunk;
 +
 +    ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
++        TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
 +        final CompoundTag entityChunk;
 +        final ChunkEntitySlices ret;
 +        final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
@@ -12856,7 +9719,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    // only to be called on the main thread, no locks need to be held
 +    public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
++        TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
 +
 +        boolean ret = false;
 +
@@ -13303,7 +10166,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
 +
 +    public SaveStat save(final boolean shutdown) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
++        TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
 +
 +        ChunkAccess chunk = this.getCurrentChunk();
 +        PoiChunk poi = this.getPoiChunk();
@@ -16035,11 +12898,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
 +import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
 +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
 +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
++import net.minecraft.server.level.ServerChunkCache;
 +import net.minecraft.server.level.ServerLevel;
 +import net.minecraft.world.level.chunk.ChunkAccess;
 +import net.minecraft.world.level.chunk.ImposterProtoChunk;
@@ -16097,6 +12961,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
 +            }
 +
++            ((ChunkSystemLevelChunk)chunk).moonrise$setChunkAndHolder(new ServerChunkCache.ChunkAndHolder(chunk, this.chunkHolder.vanillaChunkHolder));
++
 +            final NewChunkHolder chunkHolder = this.chunkHolder;
 +
 +            chunk.setFullStatus(chunkHolder::getChunkStatus);
@@ -18316,6 +15182,41 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
 +
 +}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
++
++public final class ChunkTickConstants {
++
++    public static final int PLAYER_SPAWN_TRACK_RANGE = 8;
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
++
++import net.minecraft.core.SectionPos;
++import net.minecraft.server.level.ServerPlayer;
++
++public interface ChunkTickDistanceManager {
++
++    public void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos);
++
++    public void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos);
++
++    public void moonrise$updatePlayer(final ServerPlayer player,
++                                      final SectionPos oldPos, final SectionPos newPos,
++                                      final boolean oldIgnore, final boolean newIgnore);
++
++}
 diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -24898,259 +21799,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private SaveUtil() {}
 +}
-diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-@@ -0,0 +0,0 @@ import java.util.List;
- import java.util.concurrent.CompletableFuture;
- import java.util.function.Consumer;
- 
-+/**
-+ * @deprecated Use {@link ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem}
-+ */
-+@Deprecated(forRemoval = true)
- public final class ChunkSystem {
- 
-     private static final Logger LOGGER = LogUtils.getLogger();
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
-     }
- 
-     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
--        scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run); // Paper - reroute
-     }
- 
-     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
--        level.chunkSource.mainThreadProcessor.execute(run);
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run, priority); // Paper - reroute
-     }
- 
-     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
-                                          final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
-                                          final Consumer<ChunkAccess> onComplete) {
--        if (gen) {
--            scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
--            return;
--        }
--        scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
--            if (chunk == null) {
--                if (onComplete != null) {
--                    onComplete.accept(null);
--                }
--            } else {
--                if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
--                    scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
--                } else {
--                    if (onComplete != null) {
--                        onComplete.accept(null);
--                    }
--                }
--            }
--        });
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - reroute
-     }
- 
-     static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
-     private static long chunkLoadCounter = 0L;
-     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-                                          final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
--        if (!Bukkit.isPrimaryThread()) {
--            scheduleChunkTask(level, chunkX, chunkZ, () -> {
--                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
--            }, priority);
--            return;
--        }
--
--        final int minLevel = 33 + ChunkSystem.getDistance(toStatus);
--        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
--        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
--
--        if (addTicket) {
--            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
--        }
--        level.chunkSource.runDistanceManagerUpdates();
--
--        final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
--            try {
--                if (onComplete != null) {
--                    onComplete.accept(chunk);
--                }
--            } catch (final Throwable thr) {
--                LOGGER.error("Exception handling chunk load callback", thr);
--                SneakyThrow.sneaky(thr);
--            } finally {
--                if (addTicket) {
--                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
--                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
--                }
--            }
--        };
--
--        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
--        if (holder == null || holder.getTicketLevel() > minLevel) {
--            loadCallback.accept(null);
--            return;
--        }
--
--        final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
--
--        if (loadFuture.isDone()) {
--            loadCallback.accept(loadFuture.join().orElse(null));
--            return;
--        }
--
--        loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
--            if (thr != null) {
--                loadCallback.accept(null);
--                return;
--            }
--            loadCallback.accept(result.orElse(null));
--        }, (final Runnable r) -> {
--            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
--        });
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
-     }
- 
-     public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
-                                             final FullChunkStatus toStatus, final boolean addTicket,
-                                             final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
--        // This method goes unused until the chunk system rewrite
--        if (toStatus == FullChunkStatus.INACCESSIBLE) {
--            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
--        }
--
--        if (!Bukkit.isPrimaryThread()) {
--            scheduleChunkTask(level, chunkX, chunkZ, () -> {
--                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
--            }, priority);
--            return;
--        }
--
--        final int minLevel = 33 - (toStatus.ordinal() - 1);
--        final int radius = toStatus.ordinal() - 1;
--        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
--        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
--
--        if (addTicket) {
--            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
--        }
--        level.chunkSource.runDistanceManagerUpdates();
--
--        final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
--            try {
--                if (onComplete != null) {
--                    onComplete.accept(chunk);
--                }
--            } catch (final Throwable thr) {
--                LOGGER.error("Exception handling chunk load callback", thr);
--                SneakyThrow.sneaky(thr);
--            } finally {
--                if (addTicket) {
--                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
--                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
--                }
--            }
--        };
--
--        final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
--        if (holder == null || holder.getTicketLevel() > minLevel) {
--            loadCallback.accept(null);
--            return;
--        }
--
--        final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
--        switch (toStatus) {
--            case FULL: {
--                tickingState = holder.getFullChunkFuture();
--                break;
--            }
--            case BLOCK_TICKING: {
--                tickingState = holder.getTickingChunkFuture();
--                break;
--            }
--            case ENTITY_TICKING: {
--                tickingState = holder.getEntityTickingChunkFuture();
--                break;
--            }
--            default: {
--                throw new IllegalStateException("Cannot reach here");
--            }
--        }
--
--        if (tickingState.isDone()) {
--            loadCallback.accept(tickingState.join().orElse(null));
--            return;
--        }
--
--        tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
--            if (thr != null) {
--                loadCallback.accept(null);
--                return;
--            }
--            loadCallback.accept(result.orElse(null));
--        }, (final Runnable r) -> {
--            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
--        });
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
-     }
- 
-     public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
--        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolders(level); // Paper - reroute
-     }
- 
-     public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
--        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolders(level); // Paper - reroute
-     }
- 
-     public static int getVisibleChunkHolderCount(final ServerLevel level) {
--        return level.chunkSource.chunkMap.visibleChunkMap.size();
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolderCount(level); // Paper - reroute
-     }
- 
-     public static int getUpdatingChunkHolderCount(final ServerLevel level) {
--        return level.chunkSource.chunkMap.updatingChunkMap.size();
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolderCount(level); // Paper - reroute
-     }
- 
-     public static boolean hasAnyChunkHolders(final ServerLevel level) {
-@@ -0,0 +0,0 @@ public final class ChunkSystem {
-     }
- 
-     public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
--        return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUnloadingChunkHolder(level, chunkX, chunkZ); // Paper - reroute
-     }
- 
-     public static int getSendViewDistance(final ServerPlayer player) {
--        return getLoadViewDistance(player);
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - reroute
-     }
- 
-     public static int getLoadViewDistance(final ServerPlayer player) {
--        final ServerLevel level = player.serverLevel();
--        if (level == null) {
--            return Bukkit.getViewDistance();
--        }
--        return level.chunkSource.chunkMap.getPlayerViewDistance(player);
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getLoadViewDistance(player); // Paper - reroute
-     }
- 
-     public static int getTickViewDistance(final ServerPlayer player) {
--        final ServerLevel level = player.serverLevel();
--        if (level == null) {
--            return Bukkit.getSimulationDistance();
--        }
--        return level.chunkSource.chunkMap.distanceManager.simulationDistance;
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getTickViewDistance(player); // Paper - reroute
-     }
- 
-     private ChunkSystem() {
 diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -25666,132 +22314,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +}
-diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/util/TickThread.java
-+++ b/src/main/java/io/papermc/paper/util/TickThread.java
-@@ -0,0 +0,0 @@ import net.minecraft.world.entity.Entity;
- import org.bukkit.Bukkit;
- import java.util.concurrent.atomic.AtomicInteger;
- 
--public final class TickThread extends Thread {
-+public class TickThread extends Thread {
- 
-     public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks");
- 
-@@ -0,0 +0,0 @@ public final class TickThread extends Thread {
-         }
-     }
- 
-+    /**
-+     * @deprecated
-+     */
-+    @Deprecated
-     public static void softEnsureTickThread(final String reason) {
-         if (!STRICT_THREAD_CHECKS) {
-             return;
-@@ -0,0 +0,0 @@ public final class TickThread extends Thread {
-         ensureTickThread(reason);
-     }
- 
-+    /**
-+     * @deprecated
-+     */
-+    @Deprecated
-     public static void ensureTickThread(final String reason) {
-         if (!isTickThread()) {
-             MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-@@ -0,0 +0,0 @@ public final class TickThread extends Thread {
-         }
-     }
- 
-+    public static void ensureTickThread(final ServerLevel world, final net.minecraft.core.BlockPos pos, final String reason) {
-+        if (!isTickThreadFor(world, pos)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+    public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos, final String reason) {
-+        if (!isTickThreadFor(world, pos)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+
-     public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
-         if (!isTickThreadFor(world, chunkX, chunkZ)) {
-             MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-@@ -0,0 +0,0 @@ public final class TickThread extends Thread {
-         }
-     }
- 
-+    public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.phys.AABB aabb, final String reason) {
-+        if (!isTickThreadFor(world, aabb)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+    public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
-+        if (!isTickThreadFor(world, blockX, blockZ)) {
-+            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
-+            throw new IllegalStateException(reason);
-+        }
-+    }
-+
-+
-     public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
- 
-     private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
-@@ -0,0 +0,0 @@ public final class TickThread extends Thread {
-     }
- 
-     public static boolean isTickThread() {
--        return Bukkit.isPrimaryThread();
-+        return Thread.currentThread() instanceof TickThread;
-+    }
-+
-+    public static boolean isShutdownThread() {
-+        return false;
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.core.BlockPos pos) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 pos) {
-+        return isTickThread();
-     }
- 
-     public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
-         return isTickThread();
-     }
- 
-+    public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.AABB aabb) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 position, final net.minecraft.world.phys.Vec3 deltaMovement, final int buffer) {
-+        return isTickThread();
-+    }
-+
-+    public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
-+        return isTickThread();
-+    }
-+
-     public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
-         return isTickThread();
-     }
 diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/core/Direction.java
@@ -25931,7 +22453,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
          AtomicReference<S> atomicreference = new AtomicReference();
 -        Thread thread = new Thread(() -> {
-+        Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
++        Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
              ((MinecraftServer) atomicreference.get()).runServer();
          }, "Server thread");
  
@@ -26119,7 +22641,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - rewrite chunk system
 +    @Override
 +    public boolean isSameThread() {
-+        return io.papermc.paper.util.TickThread.isTickThread();
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread();
 +    }
 +    // Paper end - rewrite chunk system
 +
@@ -26209,7 +22731,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder;
 +
 +    private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0];
-+    private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY, 0);
++    private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY);
 +
 +    private ChunkMap getChunkMap() {
 +        return (ChunkMap)this.playerProvider;
@@ -26550,7 +23072,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                chunkResult.ifSuccess(chunk -> {
 -                    if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
 -                        ChunkHolder.this.isFullChunkReady = true;
--                        io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this);
+-                        ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
 -                    }
 -                });
 -            });
@@ -26561,7 +23083,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (flag && !flag1) {
 -            // Paper start
 -            if (this.isFullChunkReady) {
--                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+-                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 -            }
 -            // Paper end
 -            this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
@@ -26579,7 +23101,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                chunkResult.ifSuccess(chunk -> {
 -                    // note: Here is a very good place to add callbacks to logic waiting on this.
 -                    ChunkHolder.this.isTickingReady = true;
--                    io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this);
+-                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
 -                });
 -            });
 -            // Paper end
@@ -26589,7 +23111,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (flag2 && !flag3) {
 -            // Paper start
 -            if (this.isTickingReady) {
--                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+-                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
 -            }
 -            // Paper end
 -            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
@@ -26610,7 +23132,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            this.entityTickingChunkFuture.thenAccept(chunkResult -> {
 -                chunkResult.ifSuccess(chunk -> {
 -                    ChunkHolder.this.isEntityTickingReady = true;
--                    io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this);
+-                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
 -                });
 -            });
 -            // Paper end
@@ -26620,7 +23142,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        if (flag4 && !flag5) {
 -            // Paper start
 -            if (this.isEntityTickingReady) {
--                io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
+-                ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
 -            }
 -            // Paper end
 -            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
@@ -26746,13 +23268,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
      public final CallbackExecutor callbackExecutor = new CallbackExecutor();
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     // Paper end
+ 
      // Paper start
      public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
--        return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-        return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
 +        return null; // Paper - rewrite chunk system
      }
-     public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
      // Paper end
  
      public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
@@ -26788,9 +23309,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.setServerViewDistance(viewDistance);
 -        this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
 +        this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null); // Paper - rewrite chunk system
-         // Paper start
-         this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
-         // Paper end
+     }
+ 
+     // Paper start
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      }
  
@@ -26942,7 +23463,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                } else {
 -                    holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
 -                    // Paper start
--                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder);
+-                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
 -                    // Paper end
 -                }
 -
@@ -26972,7 +23493,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      protected void saveAllChunks(boolean flush) {
 -        if (flush) {
--            List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
+-            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
 -            MutableBoolean mutableboolean = new MutableBoolean();
 -
 -            do {
@@ -26995,7 +23516,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            });
 -            this.flushWorker();
 -        } else {
--            io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
+-            ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
 -        }
 -
 +        ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks(
@@ -27008,7 +23529,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      public boolean hasWork() {
--        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
+-        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
 +        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
      }
  
@@ -27046,7 +23567,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        }
 -
 -        int l = 0;
--        Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+-        Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
 -
 -        while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
 -            if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
@@ -27069,7 +23590,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                // Paper start
 -                boolean removed;
 -                if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
--                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
+-                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
 -                    // Paper end
 -                    LevelChunk chunk;
 -
@@ -27089,7 +23610,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -                    this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
 -                    this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
 -                } else if (removed) { // Paper start
--                    io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
+-                    ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
 -                } // Paper end
 -
 -            }
@@ -27416,7 +23937,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
 -        return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
-+        return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
++        return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
      }
  
      private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
@@ -27490,20 +24011,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          // CraftBukkit end
      }
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+             this.updatePlayerPos(player);
+             if (!flag1) {
+                 this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
++                ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
              }
  
              player.setChunkTrackingView(ChunkTrackingView.EMPTY);
 -            this.updateChunkTracking(player);
-+            ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
-             this.addPlayerToDistanceMaps(player); // Paper - distance maps
++            ca.spottedleaf.moonrise.common.util.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
          } else {
              SectionPos sectionposition = player.getLastSectionPos();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+             this.playerMap.removePlayer(player);
+             if (!flag2) {
+                 this.distanceManager.removePlayer(sectionposition, player);
++                ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
              }
  
-             this.removePlayerFromDistanceMaps(player); // Paper - distance maps
 -            this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
-+            ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
++            ca.spottedleaf.moonrise.common.util.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
          }
  
      }
@@ -27527,6 +24054,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          SectionPos sectionposition = player.getLastSectionPos();
          SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+         if (flag2 || flag != flag1) {
+             this.updatePlayerPos(player);
++            ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation
+             if (!flag) {
+                 this.distanceManager.removePlayer(sectionposition, player);
+             }
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                  this.playerMap.unIgnorePlayer(player);
              }
  
@@ -27534,8 +24069,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // Paper - rewrite chunk system
          }
  
-         this.updateMaps(player); // Paper - distance maps
-+        ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
++        ca.spottedleaf.moonrise.common.util.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
      }
  
      private void updateChunkTracking(ServerPlayer player) {
@@ -27774,7 +24308,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        public final void moonrise$removeNonTickThreadPlayers() {
 +            boolean foundToRemove = false;
 +            for (final ServerPlayerConnection conn : this.seenBy) {
-+                if (!io.papermc.paper.util.TickThread.isTickThreadFor(conn.getPlayer())) {
++                if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) {
 +                    foundToRemove = true;
 +                    break;
 +                }
@@ -27786,7 +24320,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
 +                ServerPlayer player = conn.getPlayer();
-+                if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) {
++                if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) {
 +                    this.removePlayer(player);
 +                }
 +            }
@@ -27818,7 +24352,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import org.slf4j.Logger;
  
 -public abstract class DistanceManager {
-+public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - rewrite chunk system
++public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager { // Paper - rewrite chunk system // Paper - chunk tick iteration optimisation
  
      static final Logger LOGGER = LogUtils.getLogger();
      static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
@@ -27826,8 +24360,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
 -    public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
 -    private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
-+    // Paper - rewrite chunk system
-     private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
+-    private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
 -    private final TickingTracker tickingTicketsTracker = new TickingTracker();
 -    private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32);
 -    final Set<ChunkHolder> chunksToUpdateFutures = Sets.newHashSet();
@@ -27836,6 +24369,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -    final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
 -    final LongSet ticketsToRelease = new LongOpenHashSet();
 -    final Executor mainThreadExecutor;
++    // Paper - rewrite chunk system
++    // Paper - chunk tick iteration optimisation
 +    // Paper - rewrite chunk system
      private long ticketTickCounter;
 -    public int simulationDistance = 10;
@@ -27847,6 +24382,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager;
 +    }
 +    // Paper end - rewrite chunk system
++    // Paper start - chunk tick iteration optimisation
++    private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
++
++    @Override
++    public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) {
++        this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
++    }
++
++    @Override
++    public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) {
++        this.spawnChunkTracker.remove(player);
++    }
++
++    @Override
++    public final void moonrise$updatePlayer(final ServerPlayer player,
++                                            final SectionPos oldPos, final SectionPos newPos,
++                                            final boolean oldIgnore, final boolean newIgnore) {
++        if (newIgnore) {
++            this.spawnChunkTracker.remove(player);
++        } else {
++            this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
++        }
++    }
++    // Paper end - chunk tick iteration optimisation
 +
      protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) {
          Objects.requireNonNull(mainThreadExecutor);
@@ -28037,21 +24596,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
 @@ -0,0 +0,0 @@ public abstract class DistanceManager {
+         ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
              return new ObjectOpenHashSet();
          })).add(player);
-         this.naturalSpawnChunkCounter.update(i, 0, true);
+-        this.naturalSpawnChunkCounter.update(i, 0, true);
 -        this.playerTicketManager.update(i, 0, true);
 -        this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
++        // Paper - chunk tick iteration optimisation
 +        // Paper - rewrite chunk system
      }
  
      public void removePlayer(SectionPos pos, ServerPlayer player) {
 @@ -0,0 +0,0 @@ public abstract class DistanceManager {
+         if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
          if (objectset == null || objectset.isEmpty()) { // Paper
              this.playersPerChunk.remove(i);
-             this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
+-            this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
 -            this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
 -            this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
++            // Paper - chunk tick iteration optimisation
 +            // Paper - rewrite chunk system
          }
  
@@ -28099,7 +24662,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      }
  
-@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+     public int getNaturalSpawnChunkCount() {
+-        this.naturalSpawnChunkCounter.runAllUpdates();
+-        return this.naturalSpawnChunkCounter.chunks.size();
++        return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation
+     }
+ 
+     public boolean hasPlayersNearby(long chunkPos) {
+-        this.naturalSpawnChunkCounter.runAllUpdates();
+-        return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos);
++        return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation
      }
  
      public String getDebugStatus() {
@@ -28602,7 +25174,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            completable::complete
 +        );
 +
-+        if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
++        if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
 +            ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ);
 +            this.mainThreadProcessor.managedBlock(completable::isDone);
 +            ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait();
@@ -28733,7 +25305,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        int l = ChunkLevel.byStatus(leastStatus);
 -        ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
 +        // Paper start - rewrite chunk system
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
  
 -        // CraftBukkit start - don't add new ticket for currently unloading chunk
 -        boolean currentlyUnloading = false;
@@ -28961,6 +25533,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          // CraftBukkit end
          }
      }
+ 
+-    private static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
++    public static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) { // Paper - rewrite chunk system - public
+ 
+     }
+ }
 diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ServerEntity.java
@@ -29014,6 +25592,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private long lastMidTickFailure;
 +    private long tickedBlocksOrFluids;
 +    private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this);
++    private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
++    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
++    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
++    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
 +
 +    @Override
 +    public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
@@ -29172,6 +25754,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() {
 +        return this.nearbyPlayers;
 +    }
++
++    @Override
++    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
++        return this.loadedChunks;
++    }
++
++    @Override
++    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() {
++        return this.tickingChunks;
++    }
++
++    @Override
++    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks() {
++        return this.entityTickingChunks;
++    }
 +    // Paper end - rewrite chunk system
  
      // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
@@ -30684,7 +27281,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
 +        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
 +
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
 +
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
 +        final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
@@ -30698,7 +27295,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
 +        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
 +
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
 +
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
 +
@@ -30720,7 +27317,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
 +        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
 +
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
 +
 +        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
 +
@@ -30741,7 +27338,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system
 +        final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate);
 +        final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate);
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main");
 +        for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
 +            final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
 +            this.updateDistanceTracking(sectionPos);
@@ -30752,7 +27349,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) {
 +        final int chunkX = poiChunk.chunkX;
 +        final int chunkZ = poiChunk.chunkZ;
-+        io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main");
 +        for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) {
 +            final PoiSection section = poiChunk.getSection(sectionY);
 +            if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) {
@@ -32112,7 +28709,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          // Paper end - Perf: Optimize capturedTileEntities lookup
          // CraftBukkit end
 -        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE));
-+        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
++        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
      }
  
      public void setBlockEntity(BlockEntity blockEntity) {
@@ -32777,11 +29374,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // Paper end
 +    // Paper start - rewrite chunk system
 +    private boolean postProcessingDone;
++    private net.minecraft.server.level.ServerChunkCache.ChunkAndHolder chunkAndHolder;
 +
 +    @Override
 +    public final boolean moonrise$isPostProcessingDone() {
 +        return this.postProcessingDone;
 +    }
++
++    @Override
++    public final net.minecraft.server.level.ServerChunkCache.ChunkAndHolder moonrise$getChunkAndHolder() {
++        return this.chunkAndHolder;
++    }
++
++    @Override
++    public final void moonrise$setChunkAndHolder(final net.minecraft.server.level.ServerChunkCache.ChunkAndHolder holder) {
++        this.chunkAndHolder = holder;
++    }
 +    // Paper end - rewrite chunk system
 +    // Paper start - get block chunk optimisation
 +    private static final BlockState AIR_BLOCKSTATE = Blocks.AIR.defaultBlockState();
@@ -33372,7 +29980,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper start - async chunk saving
 +    // must be called sync
 +    public static ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
-+        io.papermc.paper.util.TickThread.ensureTickThread(world, chunk.locX, chunk.locZ, "Preparing async chunk save data");
++        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world, chunk.locX, chunk.locZ, "Preparing async chunk save data");
 +
 +        final CompoundTag tickLists = new CompoundTag();
 +        ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization());
@@ -36078,7 +32686,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @Override
      public boolean isPrimaryThread() {
 -        return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog)
-+        return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
++        return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); // Paper - rewrite chunk system
      }
  
      // Paper start - Adventure
@@ -36190,12 +32798,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
 @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
  
-     @Override
-     public int getViewDistance() {
--        return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle());
-+        return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1; // Paper - rewrite chunk system - TODO do this better
-     }
- 
      @Override
      public void setViewDistance(final int viewDistance) {
 -        throw new UnsupportedOperationException("Not implemented yet");
@@ -36264,8 +32866,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public static void catchOp(String reason)
      {
--        if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
-+        if (!io.papermc.paper.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
+-        if ( AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread )
++        if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
          {
              MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
              throw new IllegalStateException( "Asynchronous " + reason + "!" );
@@ -36278,7 +32880,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import org.bukkit.Bukkit;
  
 -public class WatchdogThread extends Thread
-+public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system
++public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
  {
  
      private static WatchdogThread instance;
diff --git a/patches/server/Optimise-general-POI-access.patch b/patches/server/Optimise-general-POI-access.patch
index 5789d3c0bf..97eaa883b3 100644
--- a/patches/server/Optimise-general-POI-access.patch
+++ b/patches/server/Optimise-general-POI-access.patch
@@ -38,6 +38,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.util;
 +
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
 +import com.mojang.datafixers.util.Pair;
 +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
 +import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
diff --git a/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch
index 40021f6b09..a0bdcc4ab3 100644
--- a/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch
+++ b/patches/server/Optimize-GoalSelector-Goal.Flag-Set-operations.patch
@@ -16,12 +16,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  public abstract class Goal {
 -    private final EnumSet<Goal.Flag> flags = EnumSet.noneOf(Goal.Flag.class);
 +    private final EnumSet<Goal.Flag> flags = EnumSet.noneOf(Goal.Flag.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be.
-+    private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector
++    private final ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector
 +
 +    // Paper start - remove streams from pathfindergoalselector; make sure types are not empty
 +    public Goal() {
 +        if (this.goalTypes.size() == 0) {
-+            this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR);
++            this.goalTypes.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
 +        }
 +    }
 +    // Paper end - remove streams from pathfindergoalselector
@@ -36,9 +36,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        this.flags.addAll(controls);
 +        // Paper start - remove streams from pathfindergoalselector
 +        this.goalTypes.clear();
-+        this.goalTypes.addAll(controls);
++        this.goalTypes.addAllUnchecked(controls);
 +        if (this.goalTypes.size() == 0) {
-+            this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR);
++            this.goalTypes.addUnchecked(Flag.UNKNOWN_BEHAVIOR);
 +        }
 +        // Paper end - remove streams from pathfindergoalselector
      }
@@ -51,7 +51,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -    public EnumSet<Goal.Flag> getFlags() {
 -        return this.flags;
 +    // Paper start - remove streams from pathfindergoalselector
-+    public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
++    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
 +        return this.goalTypes;
 +        // Paper end - remove streams from pathfindergoalselector
      }
@@ -67,7 +67,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Supplier<ProfilerFiller> profiler;
 -    private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
 +    private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector
-+    private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector
++    private final ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<net.minecraft.world.entity.ai.goal.Goal.Flag> goalTypes = new ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector
      private int curRate;
  
      public GoalSelector(Supplier<ProfilerFiller> profiler) {
@@ -84,7 +84,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -
 -        return false;
 +    // Paper start
-+    private static boolean goalContainsAnyFlags(WrappedGoal goal, com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<Goal.Flag> controls) {
++    private static boolean goalContainsAnyFlags(WrappedGoal goal, ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> controls) {
 +        return goal.getFlags().hasCommonElements(controls);
      }
  
@@ -94,7 +94,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        int wrappedGoalSize = goal.getFlags().size();
 +        for (int i = 0; i < wrappedGoalSize; ++i) {
 +            final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)];
-+            flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator);
++            flagIterator ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
 +            // Paper end
              if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) {
                  return false;
@@ -123,7 +123,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                int wrappedGoalSize = wrappedGoal2.getFlags().size();
 +                for (int i = 0; i < wrappedGoalSize; ++i) {
 +                    final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)];
-+                    flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator);
++                    flagIterator ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator);
 +                    // Paper end
                      WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL);
                      wrappedGoal3.stop();
@@ -133,12 +133,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public void disableControlFlag(Goal.Flag control) {
 -        this.disabledFlags.add(control);
-+        this.goalTypes.add(control); // Paper - remove streams from pathfindergoalselector
++        this.goalTypes.addUnchecked(control); // Paper - remove streams from pathfindergoalselector
      }
  
      public void enableControlFlag(Goal.Flag control) {
 -        this.disabledFlags.remove(control);
-+        this.goalTypes.remove(control); // Paper - remove streams from pathfindergoalselector
++        this.goalTypes.removeUnchecked(control); // Paper - remove streams from pathfindergoalselector
      }
  
      public void setControlFlag(Goal.Flag control, boolean enabled) {
@@ -152,7 +152,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @Override
 -    public EnumSet<Goal.Flag> getFlags() {
 +    // Paper start - remove streams from pathfindergoalselector
-+    public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
++    public ca.spottedleaf.moonrise.common.set.OptimizedSmallEnumSet<Goal.Flag> getFlags() {
          return this.goal.getFlags();
 +        // Paper end - remove streams from pathfindergoalselector
      }
diff --git a/patches/server/Optional-per-player-mob-spawns.patch b/patches/server/Optional-per-player-mob-spawns.patch
index ed6167d230..8e86fa60b8 100644
--- a/patches/server/Optional-per-player-mob-spawns.patch
+++ b/patches/server/Optional-per-player-mob-spawns.patch
@@ -9,9 +9,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-         return this.nearbyPlayers;
      }
  
+     // Paper start
 +    // Paper start - Optional per player mob spawns
 +    public void updatePlayerMobTypeMap(final Entity entity) {
 +        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
@@ -19,14 +19,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        final int index = entity.getType().getCategory().ordinal();
 +
-+        final com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> inRange =
-+            this.getNearbyPlayers().getPlayers(entity.chunkPosition(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++        final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> inRange =
++            this.level.moonrise$getNearbyPlayers().getPlayers(entity.chunkPosition(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
 +        if (inRange == null) {
 +            return;
 +        }
-+        final Object[] backingSet = inRange.getRawData();
++        final ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
 +        for (int i = 0, len = inRange.size(); i < len; i++) {
-+            ++((ServerPlayer)backingSet[i]).mobCounts[index];
++            ++(backingSet[i].mobCounts[index]);
 +        }
 +    }
      public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
@@ -123,12 +123,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            if (world.paperConfig().entities.spawning.perPlayerMobSpawns) {
 +                int minDiff = Integer.MAX_VALUE;
-+                final com.destroystokyo.paper.util.maplist.ReferenceList<net.minecraft.server.level.ServerPlayer> inRange =
-+                    world.chunkSource.chunkMap.getNearbyPlayers().getPlayers(chunk.getPos(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
++                final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerPlayer> inRange =
++                    world.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
 +                if (inRange != null) {
-+                    final Object[] backingSet = inRange.getRawData();
++                    final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked();
 +                    for (int k = 0, len = inRange.size(); k < len; k++) {
-+                        minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear((net.minecraft.server.level.ServerPlayer)backingSet[k], enumcreaturetype), minDiff);
++                        minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(backingSet[k], enumcreaturetype), minDiff);
 +                    }
 +                }
 +                difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
diff --git a/patches/server/PlayerNaturallySpawnCreaturesEvent.patch b/patches/server/PlayerNaturallySpawnCreaturesEvent.patch
index 5f8f541daf..1443e0faf4 100644
--- a/patches/server/PlayerNaturallySpawnCreaturesEvent.patch
+++ b/patches/server/PlayerNaturallySpawnCreaturesEvent.patch
@@ -64,9 +64,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
 +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
 @@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+     public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
      // CraftBukkit end
      public boolean isRealPlayer; // Paper
-     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
 +    public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
  
      public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
diff --git a/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch b/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
index 0f4626f556..f55b19b241 100644
--- a/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
+++ b/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
@@ -43,7 +43,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public int getTileEntityCount() {
 +        // We don't use the full world tile entity list, so we must iterate chunks
 +        int size = 0;
-+        for (ChunkHolder playerchunk : io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world)) {
++        for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world)) {
 +            net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
 +            if (chunk == null) {
 +                continue;
@@ -62,7 +62,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public int getChunkCount() {
 +        int ret = 0;
 +
-+        for (ChunkHolder chunkHolder : io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world)) {
++        for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world)) {
 +            if (chunkHolder.getTickingChunk() != null) {
 +                ++ret;
 +            }
diff --git a/patches/server/Timings-v2.patch b/patches/server/Timings-v2.patch
index 5ce5e577e6..c6f041727a 100644
--- a/patches/server/Timings-v2.patch
+++ b/patches/server/Timings-v2.patch
@@ -799,7 +799,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              MinecraftServer.LOGGER.debug("Autosave finished");
 -            SpigotTimings.worldSaveTimer.stopTiming(); // Spigot
          }
-         io.papermc.paper.util.CachedLists.reset(); // Paper
 +        // Paper start - move executeAll() into full server tick timing
 +        try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
 +            this.runAllTasks();