aboutsummaryrefslogtreecommitdiffhomepage
path: root/paper-server
diff options
context:
space:
mode:
Diffstat (limited to 'paper-server')
-rw-r--r--paper-server/patches/features/0001-Add-PaperHooks.patch345
-rw-r--r--paper-server/patches/features/0018-Moonrise-optimisation-patches.patch36330
-rw-r--r--paper-server/patches/features/0020-Rewrite-dataconverter-system.patch4
-rw-r--r--paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch12
-rw-r--r--paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch22
-rw-r--r--paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch2
-rw-r--r--paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch2
-rw-r--r--paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java5
-rw-r--r--paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java288
-rw-r--r--paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java77
-rw-r--r--paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java2
-rw-r--r--paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java8
-rw-r--r--paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java6
13 files changed, 450 insertions, 36653 deletions
diff --git a/paper-server/patches/features/0001-Add-PaperHooks.patch b/paper-server/patches/features/0001-Add-PaperHooks.patch
index db8dd7f311..5df4d535a5 100644
--- a/paper-server/patches/features/0001-Add-PaperHooks.patch
+++ b/paper-server/patches/features/0001-Add-PaperHooks.patch
@@ -6,13 +6,14 @@ Subject: [PATCH] Add PaperHooks
diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
new file mode 100644
-index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d709348ef96cc
+index 0000000000000000000000000000000000000000..2988c418b34d6f699a9c24406cfd6949465b64f0
--- /dev/null
+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -0,0 +1,240 @@
+@@ -0,0 +1,241 @@
+package ca.spottedleaf.moonrise.paper;
+
+import ca.spottedleaf.moonrise.common.PlatformHooks;
++import ca.spottedleaf.moonrise.paper.util.BaseChunkSystemHooks;
+import com.mojang.datafixers.DSL;
+import com.mojang.datafixers.DataFixer;
+import com.mojang.serialization.Dynamic;
@@ -39,7 +40,7 @@ index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d7093
+import java.util.List;
+import java.util.function.Predicate;
+
-+public final class PaperHooks implements PlatformHooks {
++public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks {
+
+ @Override
+ public String getBrand() {
@@ -250,3 +251,341 @@ index 0000000000000000000000000000000000000000..834c5ce238c7adb0164a6282582d7093
+ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
+ }
+}
+diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..34b45bc11124efb22f0f3ae5b2ad8f445c719476
+--- /dev/null
++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+@@ -0,0 +1,332 @@
++package ca.spottedleaf.moonrise.paper.util;
++
++import ca.spottedleaf.concurrentutil.util.Priority;
++import com.mojang.logging.LogUtils;
++import net.minecraft.server.level.ChunkHolder;
++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.level.ChunkPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.status.ChunkPyramid;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
++import net.minecraft.world.level.chunk.status.ChunkStep;
++import org.bukkit.Bukkit;
++import org.slf4j.Logger;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.CompletableFuture;
++import java.util.function.Consumer;
++
++public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.common.util.ChunkSystemHooks {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
++ private static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
++
++ private long chunkLoadCounter = 0L;
++
++ private static int getDistance(final ChunkStatus status) {
++ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
++ }
++
++ @Override
++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++ this.scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ @Override
++ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
++ level.chunkSource.mainThreadProcessor.execute(run);
++ }
++
++ @Override
++ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
++ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
++ final Consumer<ChunkAccess> onComplete) {
++ if (gen) {
++ this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ return;
++ }
++ this.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)) {
++ BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ } else {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ }
++ }
++ });
++ }
++
++ @Override
++ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
++ if (!Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++ this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
++ BaseChunkSystemHooks.this.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++ }, priority);
++ return;
++ }
++
++ final int minLevel = 33 + getDistance(toStatus);
++ final Long chunkReference = addTicket ? Long.valueOf(++this.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);
++ 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(ca.spottedleaf.moonrise.common.util.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) -> {
++ BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++ });
++ }
++
++ @Override
++ public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
++ final FullChunkStatus toStatus, final boolean addTicket,
++ final 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.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
++ this.scheduleChunkTask(level, chunkX, chunkZ, () -> {
++ BaseChunkSystemHooks.this.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(++this.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);
++ com.destroystokyo.paper.util.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(ca.spottedleaf.moonrise.common.util.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) -> {
++ BaseChunkSystemHooks.this.scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
++ });
++ }
++
++ @Override
++ public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
++ return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++ }
++
++ @Override
++ public List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
++ return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++ }
++
++ @Override
++ public int getVisibleChunkHolderCount(final ServerLevel level) {
++ return level.chunkSource.chunkMap.visibleChunkMap.size();
++ }
++
++ @Override
++ public int getUpdatingChunkHolderCount(final ServerLevel level) {
++ return level.chunkSource.chunkMap.updatingChunkMap.size();
++ }
++
++ @Override
++ public boolean hasAnyChunkHolders(final ServerLevel level) {
++ return this.getUpdatingChunkHolderCount(level) != 0;
++ }
++
++ @Override
++ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
++
++ }
++
++ @Override
++ public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
++ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
++ }
++
++ @Override
++ public int getSendViewDistance(final ServerPlayer player) {
++ return this.getViewDistance(player);
++ }
++
++ @Override
++ public int getViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ if (level == null) {
++ return Bukkit.getViewDistance();
++ }
++ return level.chunkSource.chunkMap.serverViewDistance;
++ }
++
++ @Override
++ public int getTickViewDistance(final ServerPlayer player) {
++ final ServerLevel level = player.serverLevel();
++ if (level == null) {
++ return Bukkit.getSimulationDistance();
++ }
++ return level.chunkSource.chunkMap.distanceManager.simulationDistance;
++ }
++
++ @Override
++ public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void updateMaps(final ServerLevel world, final ServerPlayer player) {
++
++ }
++}
diff --git a/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch
deleted file mode 100644
index dea5778bd2..0000000000
--- a/paper-server/patches/features/0018-Moonrise-optimisation-patches.patch
+++ /dev/null
@@ -1,36330 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Fri, 14 Jun 2024 11:57:26 -0700
-Subject: [PATCH] Moonrise optimisation patches
-
-Currently includes:
- - Starlight + Chunk System
- - Entity tracker optimisations
- - Collision optimisations
- - Random block ticking optimisations
- - Chunk tick iteration optimisations
- - Bitstorage optimisations
- - Block/Biome Palette read optimisations
- - StateHolder (BlockState/FluidState) property access optimisations
- - Basic Fluid property read optimisations
- - Entity/Level random replacement
-
-See https://github.com/Tuinity/Moonrise
-
-diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7e440b4a46b040365df7317035e577d93e7d855d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-@@ -0,0 +1,273 @@
-+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_system.level.ChunkSystemLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
-+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
-+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
-+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;
-+import java.util.ArrayList;
-+
-+public final class NearbyPlayers {
-+
-+ public static enum NearbyMapType {
-+ GENERAL,
-+ GENERAL_SMALL,
-+ GENERAL_REALLY_SMALL,
-+ TICK_VIEW_DISTANCE,
-+ VIEW_DISTANCE,
-+ // Moonrise start - chunk tick iteration
-+ SPAWN_RANGE {
-+ @Override
-+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
-+ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
-+ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ);
-+ }
-+ };
-+ // Moonrise end - chunk tick iteration
-+
-+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
-+
-+ }
-+
-+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
-+
-+ }
-+ }
-+
-+ 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<>();
-+ private final Long2ReferenceOpenHashMap<ReferenceList<ServerPlayer>>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES];
-+ {
-+ for (int i = 0; i < this.directByChunk.length; ++i) {
-+ this.directByChunk[i] = 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 clear() {
-+ if (this.players.isEmpty()) {
-+ return;
-+ }
-+
-+ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) {
-+ this.removePlayer(player);
-+ }
-+ }
-+
-+ 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.getViewDistance(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 TrackedChunk getChunk(final int chunkX, final int chunkZ) {
-+ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
-+ }
-+
-+ public static final class TrackedChunk {
-+
-+ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
-+
-+ private final long chunkKey;
-+ private final NearbyPlayers nearbyPlayers;
-+ private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
-+ private int nonEmptyLists;
-+ private long updateCount;
-+
-+ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) {
-+ this.chunkKey = chunkKey;
-+ this.nearbyPlayers = nearbyPlayers;
-+ }
-+
-+ 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;
-+ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY));
-+ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players);
-+ players.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.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
-+ --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);
-+
-+ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
-+ final NearbyMapType type = this.type;
-+ if (chunk != null) {
-+ chunk.addPlayer(parameter, type);
-+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
-+ } else {
-+ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this);
-+ NearbyPlayers.this.byChunk.put(chunkKey, created);
-+ created.addPlayer(parameter, type);
-+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
-+
-+ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created;
-+ }
-+ }
-+
-+ @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));
-+ }
-+
-+ final NearbyMapType type = this.type;
-+ chunk.removePlayer(parameter, type);
-+ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
-+
-+ if (chunk.isEmpty()) {
-+ NearbyPlayers.this.byChunk.remove(chunkKey);
-+ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey);
-+ if (chunkData != null) {
-+ chunkData.nearbyPlayers = null;
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-index 58a99bc38e137431f10af36fa9e2d04fe61694aa..1d288e73fd8605676c0da676e068afb5b4b8abea 100644
---- a/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-+++ b/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-@@ -2,11 +2,17 @@ package ca.spottedleaf.moonrise.common.util;
-
- import ca.spottedleaf.concurrentutil.util.Priority;
- import ca.spottedleaf.moonrise.common.PlatformHooks;
-+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 ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
- 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.server.level.progress.ChunkProgressListener;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.chunk.ChunkAccess;
- import net.minecraft.world.level.chunk.LevelChunk;
-@@ -18,203 +24,46 @@ 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, Priority.NORMAL);
- }
-
- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final 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 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 Priority priority, final Consumer<ChunkAccess> onComplete) {
-- if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
-- 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, 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 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.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
-- 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, 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) {
-@@ -233,55 +82,96 @@ public final class ChunkSystem {
- }
-
- public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-+ // Update progress listener for LevelLoadingScreen
-+ final ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener;
-+ if (progressListener != null) {
-+ ChunkSystem.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> {
-+ progressListener.onStatusChange(holder.getPos(), null);
-+ });
-+ }
-+ }
-
-+ 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()
-+ );
-+ chunk.loadCallback();
- }
-
- public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
-+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
-+ );
-+ chunk.unloadCallback();
-+ }
-
-+ 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());
-+ }
-+ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
-+ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
-+ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
- }
-
- public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
--
-+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
-+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
-+ );
-+ ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
- }
-
- 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 getViewDistance(player);
-+ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
- }
-
- public static int getViewDistance(final ServerPlayer player) {
-- final ServerLevel level = player.serverLevel();
-- if (level == null) {
-- return org.bukkit.Bukkit.getViewDistance();
-- }
-- return level.chunkSource.chunkMap.serverViewDistance;
-+ return RegionizedPlayerChunkLoader.getAPIViewDistance(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/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-index 12eb3add0931a4d77acdf6e875c42dda9c313dc3..5239993a681d6113eec99fa627b85508656ed7ac 100644
---- a/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-+++ b/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
-@@ -9,7 +9,7 @@ import net.minecraft.world.level.levelgen.PositionalRandomFactory;
- /**
- * Avoid costly CAS of superclass
- */
--public final class ThreadUnsafeRandom implements BitRandomSource {
-+public class ThreadUnsafeRandom implements BitRandomSource { // Paper - replace random
-
- private static final long MULTIPLIER = 25214903917L;
- private static final long ADDEND = 11L;
-diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 11cfe9cc29666ce3a6a40281069fb9eb4fa0ded2..de22cfd2da4782072584d5140ce5567780d6feaa 100644
---- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
-+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -267,7 +267,7 @@ public final class PaperHooks implements PlatformHooks {
-
- @Override
- public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
-- net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
-+ net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities(), chunk.getPos()); // Paper - rewrite chunk system - add ChunkPos param
- }
-
- @Override
-diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-@@ -0,0 +1,10 @@
-+package ca.spottedleaf.moonrise.patches.block_counting;
-+
-+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortArrayList;
-+
-+public interface BlockCountingBitStorage {
-+
-+ public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-@@ -0,0 +1,11 @@
-+package ca.spottedleaf.moonrise.patches.block_counting;
-+
-+import ca.spottedleaf.moonrise.common.list.ShortList;
-+
-+public interface BlockCountingChunkSection {
-+
-+ public boolean moonrise$hasSpecialCollidingBlocks();
-+
-+ public ShortList moonrise$getTickingBlockList();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess;
-+
-+public interface PropertyAccess<T> {
-+
-+ public int moonrise$getId();
-+
-+ public int moonrise$getIdFor(final T value);
-+
-+ public T moonrise$getById(final int id);
-+
-+ public void moonrise$setById(final T[] values);
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
-@@ -0,0 +1,7 @@
-+package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess;
-+
-+public interface PropertyAccessStateHolder {
-+
-+ public long moonrise$getTableIndex();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..866f38eb0f379ffbe2888023a7d1c290f521a231
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
-@@ -0,0 +1,230 @@
-+package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util;
-+
-+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
-+import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess;
-+import ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder;
-+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.AbstractObjectSet;
-+import it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.ObjectIterator;
-+import it.unimi.dsi.fastutil.objects.ObjectSet;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
-+import java.util.ArrayList;
-+import java.util.Collection;
-+import java.util.Collections;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Map;
-+import net.minecraft.world.level.block.state.StateHolder;
-+import net.minecraft.world.level.block.state.properties.Property;
-+
-+public final class ZeroCollidingReferenceStateTable<O, S> {
-+
-+ private final Int2ObjectOpenHashMap<Indexer> propertyToIndexer;
-+ private S[] lookup;
-+ private final Collection<Property<?>> properties;
-+
-+ public ZeroCollidingReferenceStateTable(final Collection<Property<?>> properties) {
-+ this.propertyToIndexer = new Int2ObjectOpenHashMap<>(properties.size());
-+ this.properties = new ReferenceArrayList<>(properties);
-+
-+ final List<Property<?>> sortedProperties = new ArrayList<>(properties);
-+
-+ // important that each table sees the same property order given the same _set_ of properties,
-+ // as each table will calculate the index for the block state
-+ sortedProperties.sort((final Property<?> p1, final Property<?> p2) -> {
-+ return Integer.compare(
-+ ((PropertyAccess<?>)p1).moonrise$getId(),
-+ ((PropertyAccess<?>)p2).moonrise$getId()
-+ );
-+ });
-+
-+ int currentMultiple = 1;
-+ for (final Property<?> property : sortedProperties) {
-+ final int totalValues = property.getPossibleValues().size();
-+
-+ this.propertyToIndexer.put(
-+ ((PropertyAccess<?>)property).moonrise$getId(),
-+ new Indexer(
-+ totalValues,
-+ currentMultiple,
-+ IntegerUtil.getUnsignedDivisorMagic((long)currentMultiple, 32),
-+ IntegerUtil.getUnsignedDivisorMagic((long)totalValues, 32)
-+ )
-+ );
-+
-+ currentMultiple *= totalValues;
-+ }
-+ }
-+
-+ public <T extends Comparable<T>> boolean hasProperty(final Property<T> property) {
-+ return this.propertyToIndexer.containsKey(((PropertyAccess<T>)property).moonrise$getId());
-+ }
-+
-+ public long getIndex(final StateHolder<O, S> stateHolder) {
-+ long ret = 0L;
-+
-+ for (final Map.Entry<Property<?>, Comparable<?>> entry : stateHolder.getValues().entrySet()) {
-+ final Property<?> property = entry.getKey();
-+ final Comparable<?> value = entry.getValue();
-+
-+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<?>)property).moonrise$getId());
-+
-+ ret += (((PropertyAccess)property).moonrise$getIdFor(value)) * indexer.multiple;
-+ }
-+
-+ return ret;
-+ }
-+
-+ public boolean isLoaded() {
-+ return this.lookup != null;
-+ }
-+
-+ public void loadInTable(final Map<Map<Property<?>, Comparable<?>>, S> universe) {
-+ if (this.lookup != null) {
-+ throw new IllegalStateException();
-+ }
-+
-+ this.lookup = (S[])new StateHolder[universe.size()];
-+
-+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : universe.entrySet()) {
-+ final S value = entry.getValue();
-+ if (value == null) {
-+ continue;
-+ }
-+ this.lookup[(int)((PropertyAccessStateHolder)(StateHolder<O, S>)value).moonrise$getTableIndex()] = value;
-+ }
-+
-+ for (final S value : this.lookup) {
-+ if (value == null) {
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ public <T extends Comparable<T>> T get(final long index, final Property<T> property) {
-+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId());
-+ if (indexer == null) {
-+ return null;
-+ }
-+
-+ final long divided = (index * indexer.multipleDivMagic) >>> 32;
-+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32;
-+ // equiv to: divided = index / multiple
-+ // modded = divided % totalValues
-+
-+ return ((PropertyAccess<T>)property).moonrise$getById((int)modded);
-+ }
-+
-+ public <T extends Comparable<T>> S set(final long index, final Property<T> property, final T with) {
-+ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with);
-+ if (newValueId < 0) {
-+ return null;
-+ }
-+
-+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId());
-+ if (indexer == null) {
-+ return null;
-+ }
-+
-+ final long divided = (index * indexer.multipleDivMagic) >>> 32;
-+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32;
-+ // equiv to: divided = index / multiple
-+ // modded = divided % totalValues
-+
-+ // subtract out the old value, add in the new
-+ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index;
-+
-+ return this.lookup[(int)newIndex];
-+ }
-+
-+ public <T extends Comparable<T>> S trySet(final long index, final Property<T> property, final T with, final S dfl) {
-+ final Indexer indexer = this.propertyToIndexer.get(((PropertyAccess<T>)property).moonrise$getId());
-+ if (indexer == null) {
-+ return dfl;
-+ }
-+
-+ final int newValueId = ((PropertyAccess<T>)property).moonrise$getIdFor(with);
-+ if (newValueId < 0) {
-+ return null;
-+ }
-+
-+ final long divided = (index * indexer.multipleDivMagic) >>> 32;
-+ final long modded = (((divided * indexer.modMagic) & 0xFFFFFFFFL) * indexer.totalValues) >>> 32;
-+ // equiv to: divided = index / multiple
-+ // modded = divided % totalValues
-+
-+ // subtract out the old value, add in the new
-+ final long newIndex = (((long)newValueId - modded) * indexer.multiple) + index;
-+
-+ return this.lookup[(int)newIndex];
-+ }
-+
-+ public Collection<Property<?>> getProperties() {
-+ return Collections.unmodifiableCollection(this.properties);
-+ }
-+
-+ public Map<Property<?>, Comparable<?>> getMapView(final long stateIndex) {
-+ return new MapView(stateIndex);
-+ }
-+
-+ private static final record Indexer(
-+ int totalValues, int multiple, long multipleDivMagic, long modMagic
-+ ) {}
-+
-+ private class MapView extends AbstractReference2ObjectMap<Property<?>, Comparable<?>> {
-+ private final long stateIndex;
-+ private EntrySet entrySet;
-+
-+ MapView(final long stateIndex) {
-+ this.stateIndex = stateIndex;
-+ }
-+
-+ @Override
-+ public boolean containsKey(final Object key) {
-+ return key instanceof Property<?> prop && ZeroCollidingReferenceStateTable.this.hasProperty(prop);
-+ }
-+
-+ @Override
-+ public int size() {
-+ return ZeroCollidingReferenceStateTable.this.properties.size();
-+ }
-+
-+ @Override
-+ public ObjectSet<Entry<Property<?>, Comparable<?>>> reference2ObjectEntrySet() {
-+ if (this.entrySet == null)
-+ this.entrySet = new EntrySet();
-+ return this.entrySet;
-+ }
-+
-+ @Override
-+ public Comparable<?> get(final Object key) {
-+ return key instanceof Property<?> prop ? ZeroCollidingReferenceStateTable.this.get(this.stateIndex, prop) : null;
-+ }
-+
-+ class EntrySet extends AbstractObjectSet<Entry<Property<?>, Comparable<?>>> {
-+ @Override
-+ public ObjectIterator<Reference2ObjectMap.Entry<Property<?>, Comparable<?>>> iterator() {
-+ final Iterator<Property<?>> propIterator = ZeroCollidingReferenceStateTable.this.properties.iterator();
-+ return new ObjectIterator<>() {
-+ @Override
-+ public boolean hasNext() {
-+ return propIterator.hasNext();
-+ }
-+
-+ @Override
-+ public Entry<Property<?>, Comparable<?>> next() {
-+ Property<?> prop = propIterator.next();
-+ return new AbstractReference2ObjectMap.BasicEntry<>(prop, ZeroCollidingReferenceStateTable.this.get(MapView.this.stateIndex, prop));
-+ }
-+ };
-+ }
-+
-+ @Override
-+ public int size() {
-+ return ZeroCollidingReferenceStateTable.this.properties.size();
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-@@ -0,0 +1,39 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import net.minecraft.SharedConstants;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.datafix.fixes.References;
-+
-+public final class ChunkSystemConverters {
-+
-+ // See SectionStorage#getVersion
-+ private static final int DEFAULT_POI_DATA_VERSION = 1945;
-+
-+ private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1;
-+
-+ private static int getCurrentVersion() {
-+ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
-+ }
-+
-+ private static int getDataVersion(final CompoundTag data, final int dfl) {
-+ return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC)
-+ ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG);
-+ }
-+
-+ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
-+ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
-+
-+ return PlatformHooks.get().convertNBT(References.POI_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
-+ }
-+
-+ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) {
-+ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION);
-+
-+ return PlatformHooks.get().convertNBT(References.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
-+ }
-+
-+ private ChunkSystemConverters() {}
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-@@ -0,0 +1,44 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.entity;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.monster.Shulker;
-+import net.minecraft.world.entity.vehicle.AbstractMinecart;
-+import net.minecraft.world.entity.vehicle.Boat;
-+
-+public interface ChunkSystemEntity {
-+
-+ public boolean moonrise$isHardColliding();
-+
-+ // for mods to override
-+ public default boolean moonrise$isHardCollidingUncached() {
-+ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith();
-+ }
-+
-+ public FullChunkStatus moonrise$getChunkStatus();
-+
-+ public void moonrise$setChunkStatus(final FullChunkStatus status);
-+
-+ public ChunkData moonrise$getChunkData();
-+
-+ public void moonrise$setChunkData(final ChunkData chunkData);
-+
-+ public int moonrise$getSectionX();
-+
-+ public void moonrise$setSectionX(final int x);
-+
-+ public int moonrise$getSectionY();
-+
-+ public void moonrise$setSectionY(final int y);
-+
-+ public int moonrise$getSectionZ();
-+
-+ public void moonrise$setSectionZ(final int z);
-+
-+ public boolean moonrise$isUpdatingSectionStatus();
-+
-+ public void moonrise$setUpdatingSectionStatus(final boolean to);
-+
-+ public boolean moonrise$hasAnyPlayerPassengers();
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-@@ -0,0 +1,31 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.io;
-+
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.world.level.chunk.storage.RegionFile;
-+import java.io.IOException;
-+
-+public interface ChunkSystemRegionFileStorage {
-+
-+ public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ);
-+
-+ public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ);
-+
-+ public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException;
-+
-+ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(
-+ final int chunkX, final int chunkZ, final CompoundTag compound
-+ ) throws IOException;
-+
-+ public void moonrise$finishWrite(
-+ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.WriteData writeData
-+ ) throws IOException;
-+
-+ public MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData(
-+ final int chunkX, final int chunkZ
-+ ) throws IOException;
-+
-+ // if the return value is null, then the caller needs to re-try with a new call to readData()
-+ public CompoundTag moonrise$finishRead(
-+ final int chunkX, final int chunkZ, final MoonriseRegionFileIO.RegionDataController.ReadData readData
-+ ) throws IOException;
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1acea58838f057ab87efd103cbecb6f5aeaef393
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
-@@ -0,0 +1,1700 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.io;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
-+import ca.spottedleaf.concurrentutil.completable.Completable;
-+import ca.spottedleaf.concurrentutil.executor.Cancellable;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue;
-+import ca.spottedleaf.concurrentutil.function.BiLong1Function;
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+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 it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.storage.RegionFile;
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.io.DataInputStream;
-+import java.io.DataOutputStream;
-+import java.io.IOException;
-+import java.lang.invoke.VarHandle;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.CompletionException;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.BiConsumer;
-+import java.util.function.Consumer;
-+
-+public final class MoonriseRegionFileIO {
-+
-+ private static final int REGION_FILE_SHIFT = 5;
-+ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseRegionFileIO.class);
-+
-+ /**
-+ * The types of RegionFiles controlled by the I/O thread(s).
-+ */
-+ public static enum RegionFileType {
-+ CHUNK_DATA,
-+ POI_DATA,
-+ ENTITY_DATA;
-+ }
-+
-+ public static RegionDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
-+ switch (type) {
-+ case CHUNK_DATA:
-+ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController();
-+ case POI_DATA:
-+ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController();
-+ case ENTITY_DATA:
-+ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController();
-+ default:
-+ throw new IllegalStateException("Unknown controller type " + type);
-+ }
-+ }
-+
-+ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
-+
-+ /**
-+ * Collects RegionFile data for a certain chunk.
-+ */
-+ public static final class RegionFileData {
-+
-+ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length];
-+ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length];
-+ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length];
-+
-+ /**
-+ * Sets the result associated with the specified RegionFile type. Note that
-+ * results can only be set once per RegionFile type.
-+ *
-+ * @param type The RegionFile type.
-+ * @param data The result to set.
-+ */
-+ public void setData(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) {
-+ final int index = type.ordinal();
-+
-+ if (this.hasResult[index]) {
-+ throw new IllegalArgumentException("Result already exists for type " + type);
-+ }
-+ this.hasResult[index] = true;
-+ this.data[index] = data;
-+ }
-+
-+ /**
-+ * Sets the result associated with the specified RegionFile type. Note that
-+ * results can only be set once per RegionFile type.
-+ *
-+ * @param type The RegionFile type.
-+ * @param throwable The result to set.
-+ */
-+ public void setThrowable(final MoonriseRegionFileIO.RegionFileType type, final Throwable throwable) {
-+ final int index = type.ordinal();
-+
-+ if (this.hasResult[index]) {
-+ throw new IllegalArgumentException("Result already exists for type " + type);
-+ }
-+ this.hasResult[index] = true;
-+ this.throwables[index] = throwable;
-+ }
-+
-+ /**
-+ * Returns whether there is a result for the specified RegionFile type.
-+ *
-+ * @param type Specified RegionFile type.
-+ *
-+ * @return Whether a result exists for {@code type}.
-+ */
-+ public boolean hasResult(final MoonriseRegionFileIO.RegionFileType type) {
-+ return this.hasResult[type.ordinal()];
-+ }
-+
-+ /**
-+ * Returns the data result for the RegionFile type.
-+ *
-+ * @param type Specified RegionFile type.
-+ *
-+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
-+ * @return The data result for the specified type. If the result is a {@code Throwable},
-+ * then returns {@code null}.
-+ */
-+ public CompoundTag getData(final MoonriseRegionFileIO.RegionFileType type) {
-+ final int index = type.ordinal();
-+
-+ if (!this.hasResult[index]) {
-+ throw new IllegalArgumentException("Result does not exist for type " + type);
-+ }
-+
-+ return this.data[index];
-+ }
-+
-+ /**
-+ * Returns the throwable result for the RegionFile type.
-+ *
-+ * @param type Specified RegionFile type.
-+ *
-+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
-+ * @return The throwable result for the specified type. If the result is an {@code CompoundTag},
-+ * then returns {@code null}.
-+ */
-+ public Throwable getThrowable(final MoonriseRegionFileIO.RegionFileType type) {
-+ final int index = type.ordinal();
-+
-+ if (!this.hasResult[index]) {
-+ throw new IllegalArgumentException("Result does not exist for type " + type);
-+ }
-+
-+ return this.throwables[index];
-+ }
-+ }
-+
-+ public static void flushRegionStorages(final ServerLevel world) throws IOException {
-+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ flushRegionStorages(world, type);
-+ }
-+ }
-+
-+ public static void flushRegionStorages(final ServerLevel world, final RegionFileType type) throws IOException {
-+ getControllerFor(world, type).getCache().flush();
-+ }
-+
-+ public static void flush(final MinecraftServer server) {
-+ for (final ServerLevel world : server.getAllLevels()) {
-+ flush(world);
-+ }
-+ }
-+
-+ public static void flush(final ServerLevel world) {
-+ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) {
-+ flush(world, regionFileType);
-+ }
-+ }
-+
-+ public static void flush(final ServerLevel world, final RegionFileType type) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+
-+ long failures = 1L; // start at 0.13ms
-+
-+ while (taskController.hasTasks()) {
-+ Thread.yield();
-+ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms
-+ }
-+ }
-+
-+ public static void partialFlush(final ServerLevel world, final int tasksRemaining) {
-+ for (long failures = 1L;;) { // start at 0.13ms
-+ long totalTasks = 0L;
-+ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) {
-+ totalTasks += getControllerFor(world, regionFileType).getTotalWorkingTasks();
-+ }
-+
-+ if (totalTasks > (long)tasksRemaining) {
-+ Thread.yield();
-+ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms
-+ } else {
-+ return;
-+ }
-+ }
-+ }
-+
-+ /**
-+ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid
-+ * dumb plugins from taking away priority from threads we consider crucial.
-+ * @return The priroity to use with blocking I/O on the current thread.
-+ */
-+ public static Priority getIOBlockingPriorityForCurrentThread() {
-+ if (TickThread.isTickThread()) {
-+ return Priority.BLOCKING;
-+ }
-+ return Priority.HIGHEST;
-+ }
-+
-+ /**
-+ * Returns the priority for the specified regionfile type for the specified chunk.
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param type Specified regionfile type.
-+ * @return The priority for the chunk
-+ */
-+ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ if (task == null) {
-+ return Priority.COMPLETING;
-+ }
-+
-+ return task.getPriority();
-+ }
-+
-+ /**
-+ * Sets the priority for all regionfile types for the specified chunk. Note that great care should
-+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
-+ * priorities.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param priority New priority.
-+ *
-+ * @see #raisePriority(ServerLevel, int, int, Priority)
-+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Priority priority) {
-+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ MoonriseRegionFileIO.setPriority(world, chunkX, chunkZ, type, priority);
-+ }
-+ }
-+
-+ /**
-+ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should
-+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
-+ * priorities.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param type Specified regionfile type.
-+ * @param priority New priority.
-+ *
-+ * @see #raisePriority(ServerLevel, int, int, Priority)
-+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ if (task != null) {
-+ task.setPriority(priority);
-+ }
-+ }
-+
-+ /**
-+ * Raises the priority for all regionfile types for the specified chunk.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param priority New priority.
-+ *
-+ * @see #setPriority(ServerLevel, int, int, Priority)
-+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Priority priority) {
-+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ MoonriseRegionFileIO.raisePriority(world, chunkX, chunkZ, type, priority);
-+ }
-+ }
-+
-+ /**
-+ * Raises the priority for the specified regionfile type for the specified chunk.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param type Specified regionfile type.
-+ * @param priority New priority.
-+ *
-+ * @see #setPriority(ServerLevel, int, int, Priority)
-+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, Priority)
-+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ if (task != null) {
-+ task.raisePriority(priority);
-+ }
-+ }
-+
-+ /**
-+ * Lowers the priority for all regionfile types for the specified chunk.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param priority New priority.
-+ *
-+ * @see #raisePriority(ServerLevel, int, int, Priority)
-+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #setPriority(ServerLevel, int, int, Priority)
-+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Priority priority) {
-+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ MoonriseRegionFileIO.lowerPriority(world, chunkX, chunkZ, type, priority);
-+ }
-+ }
-+
-+ /**
-+ * Lowers the priority for the specified regionfile type for the specified chunk.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param type Specified regionfile type.
-+ * @param priority New priority.
-+ *
-+ * @see #raisePriority(ServerLevel, int, int, Priority)
-+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-+ * @see #setPriority(ServerLevel, int, int, Priority)
-+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-+ */
-+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ if (task != null) {
-+ task.lowerPriority(priority);
-+ }
-+ }
-+
-+ /**
-+ * Schedules the chunk data to be written asynchronously.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
-+ * saves must be scheduled before a chunk is unloaded.
-+ * </li>
-+ * <li>
-+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param data Chunk's data
-+ * @param type The regionfile type to write to.
-+ *
-+ * @throws IllegalStateException If the file io thread has shutdown.
-+ */
-+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-+ final RegionFileType type) {
-+ MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
-+ }
-+
-+ /**
-+ * Schedules the chunk data to be written asynchronously.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
-+ * saves must be scheduled before a chunk is unloaded.
-+ * </li>
-+ * <li>
-+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param data Chunk's data
-+ * @param type The regionfile type to write to.
-+ * @param priority The minimum priority to schedule at.
-+ *
-+ * @throws IllegalStateException If the file io thread has shutdown.
-+ */
-+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-+ final RegionFileType type, final Priority priority) {
-+ scheduleSave(
-+ world, chunkX, chunkZ,
-+ (final BiConsumer<CompoundTag, Throwable> consumer) -> {
-+ consumer.accept(data, null);
-+ }, null, type, priority
-+ );
-+ }
-+
-+ /**
-+ * Schedules the chunk data to be written asynchronously.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
-+ * saves must be scheduled before a chunk is unloaded.
-+ * </li>
-+ * <li>
-+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ * <li>
-+ * The specified write task, if not null, will have its priority controlled by the scheduler.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param completable Chunk's pending data
-+ * @param writeTask The task responsible for completing the pending chunk data
-+ * @param type The regionfile type to write to.
-+ * @param priority The minimum priority to schedule at.
-+ *
-+ * @throws IllegalStateException If the file io thread has shutdown.
-+ */
-+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CallbackCompletable<CompoundTag> completable,
-+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) {
-+ scheduleSave(world, chunkX, chunkZ, completable::addWaiter, writeTask, type, priority);
-+ }
-+
-+ /**
-+ * Schedules the chunk data to be written asynchronously.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
-+ * saves must be scheduled before a chunk is unloaded.
-+ * </li>
-+ * <li>
-+ * Writes may be called concurrently, although only the "later" write will go through.
-+ * </li>
-+ * <li>
-+ * The specified write task, if not null, will have its priority controlled by the scheduler.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param completable Chunk's pending data
-+ * @param writeTask The task responsible for completing the pending chunk data
-+ * @param type The regionfile type to write to.
-+ * @param priority The minimum priority to schedule at.
-+ *
-+ * @throws IllegalStateException If the file io thread has shutdown.
-+ */
-+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Completable<CompoundTag> completable,
-+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) {
-+ scheduleSave(world, chunkX, chunkZ, completable::whenComplete, writeTask, type, priority);
-+ }
-+
-+ private static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler,
-+ final PrioritisedExecutor.PrioritisedTask writeTask, final RegionFileType type, final Priority priority) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+
-+ final boolean[] created = new boolean[1];
-+ final ChunkIOTask.InProgressWrite write = new ChunkIOTask.InProgressWrite(writeTask);
-+ final ChunkIOTask task = taskController.chunkTasks.compute(CoordinateUtils.getChunkKey(chunkX, chunkZ),
-+ (final long keyInMap, final ChunkIOTask taskRunning) -> {
-+ if (taskRunning == null || taskRunning.failedWrite) {
-+ // no task is scheduled or the previous write failed - meaning we need to overwrite it
-+
-+ // create task
-+ final ChunkIOTask newTask = new ChunkIOTask(
-+ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead()
-+ );
-+
-+ newTask.pushPendingWrite(write);
-+
-+ created[0] = true;
-+
-+ return newTask;
-+ }
-+
-+ taskRunning.pushPendingWrite(write);
-+
-+ return taskRunning;
-+ }
-+ );
-+
-+ write.schedule(task, scheduler);
-+
-+ if (created[0]) {
-+ taskController.startTask(task);
-+ task.scheduleWriteCompress();
-+ } else {
-+ task.raisePriority(priority);
-+ }
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
-+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
-+ * for single load.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
-+ */
-+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) {
-+ return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
-+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
-+ * for single load.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ * @param priority The minimum priority to load the data at.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
-+ */
-+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
-+ final Priority priority) {
-+ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
-+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
-+ * for single load.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ * @param types The regionfile type(s) to load.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
-+ */
-+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
-+ final RegionFileType... types) {
-+ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
-+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
-+ * for single load.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ * @param types The regionfile type(s) to load.
-+ * @param priority The minimum priority to load the data at.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
-+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
-+ */
-+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
-+ final Priority priority, final RegionFileType... types) {
-+ if (types == null) {
-+ throw new NullPointerException("Types cannot be null");
-+ }
-+ if (types.length == 0) {
-+ throw new IllegalArgumentException("Types cannot be empty");
-+ }
-+
-+ final RegionFileData ret = new RegionFileData();
-+
-+ final Cancellable[] reads = new CancellableRead[types.length];
-+ final AtomicInteger completions = new AtomicInteger();
-+ final int expectedCompletions = types.length;
-+
-+ for (int i = 0; i < expectedCompletions; ++i) {
-+ final RegionFileType type = types[i];
-+ reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type,
-+ (final CompoundTag data, final Throwable throwable) -> {
-+ if (throwable != null) {
-+ ret.setThrowable(type, throwable);
-+ } else {
-+ ret.setData(type, data);
-+ }
-+
-+ if (completions.incrementAndGet() == expectedCompletions) {
-+ onComplete.accept(ret);
-+ }
-+ }, intendingToBlock, priority);
-+ }
-+
-+ return new CancellableReads(reads);
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
-+ * {@code onComplete}.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
-+ */
-+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final boolean intendingToBlock) {
-+ return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
-+ }
-+
-+ /**
-+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
-+ * {@code onComplete}.
-+ * <p>
-+ * Impl notes:
-+ * </p>
-+ * <li>
-+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
-+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
-+ * data is undefined behaviour, and can cause deadlock.
-+ * </li>
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param onComplete Consumer to execute once this task has completed
-+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
-+ * of this call.
-+ * @param priority Minimum priority to load the data at.
-+ *
-+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
-+ *
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
-+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
-+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
-+ */
-+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final boolean intendingToBlock, final Priority priority) {
-+ final RegionDataController taskController = getControllerFor(world, type);
-+
-+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
-+
-+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final BiLong1Function<ChunkIOTask, ChunkIOTask> compute = (final long keyInMap, final ChunkIOTask running) -> {
-+ if (running == null) {
-+ // not scheduled
-+
-+ // set up task
-+ final ChunkIOTask newTask = new ChunkIOTask(
-+ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead()
-+ );
-+ newTask.inProgressRead.addToAsyncWaiters(onComplete);
-+
-+ callbackInfo.tasksNeedReadScheduling = true;
-+ return newTask;
-+ }
-+
-+ final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite;
-+
-+ if (pendingWrite == null) {
-+ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations
-+ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) {
-+ callbackInfo.data = running.inProgressRead.value;
-+ callbackInfo.throwable = running.inProgressRead.throwable;
-+ callbackInfo.completeNow = true;
-+ return running;
-+ }
-+
-+ callbackInfo.read = running.inProgressRead;
-+
-+ return running;
-+ }
-+
-+ // at this stage we have to use the in progress write's data to avoid an order issue
-+
-+ if (!pendingWrite.addToAsyncWaiters(onComplete)) {
-+ // data is ready now
-+ callbackInfo.data = pendingWrite.value;
-+ callbackInfo.throwable = pendingWrite.throwable;
-+ callbackInfo.completeNow = true;
-+ return running;
-+ }
-+
-+ callbackInfo.write = pendingWrite;
-+
-+ return running;
-+ };
-+
-+ final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute);
-+
-+ // needs to be scheduled
-+ if (callbackInfo.tasksNeedReadScheduling) {
-+ taskController.startTask(ret);
-+ ret.scheduleReadIO();
-+ } else if (callbackInfo.completeNow) {
-+ try {
-+ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr);
-+ }
-+ } else {
-+ // we're waiting on a task we didn't schedule, so raise its priority to what we want
-+ ret.raisePriority(priority);
-+ }
-+
-+ return new CancellableRead(onComplete, callbackInfo.read, callbackInfo.write);
-+ }
-+
-+ private static final class ImmediateCallbackCompletion {
-+
-+ private CompoundTag data;
-+ private Throwable throwable;
-+ private boolean completeNow;
-+ private boolean tasksNeedReadScheduling;
-+ private ChunkIOTask.InProgressRead read;
-+ private ChunkIOTask.InProgressWrite write;
-+
-+ }
-+
-+ /**
-+ * Schedules a load task to be executed asynchronously, and blocks on that task.
-+ *
-+ * @param world Chunk's world
-+ * @param chunkX Chunk's x coordinate
-+ * @param chunkZ Chunk's z coordinate
-+ * @param type Regionfile type
-+ * @param priority Minimum priority to load the data at.
-+ *
-+ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk.
-+ *
-+ * @throws IOException If the load fails for any reason
-+ */
-+ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) throws IOException {
-+ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>();
-+
-+ MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
-+ if (thr != null) {
-+ ret.completeExceptionally(thr);
-+ } else {
-+ ret.complete(compound);
-+ }
-+ }, true, priority);
-+
-+ try {
-+ return ret.join();
-+ } catch (final CompletionException ex) {
-+ throw new IOException(ex);
-+ }
-+ }
-+
-+ private static final class CancellableRead implements Cancellable {
-+
-+ private BiConsumer<CompoundTag, Throwable> callback;
-+ private ChunkIOTask.InProgressRead read;
-+ private ChunkIOTask.InProgressWrite write;
-+
-+ private CancellableRead(final BiConsumer<CompoundTag, Throwable> callback,
-+ final ChunkIOTask.InProgressRead read,
-+ final ChunkIOTask.InProgressWrite write) {
-+ this.callback = callback;
-+ this.read = read;
-+ this.write = write;
-+ }
-+
-+ @Override
-+ public boolean cancel() {
-+ final BiConsumer<CompoundTag, Throwable> callback = this.callback;
-+ final ChunkIOTask.InProgressRead read = this.read;
-+ final ChunkIOTask.InProgressWrite write = this.write;
-+
-+ if (callback == null || (read == null && write == null)) {
-+ return false;
-+ }
-+
-+ this.callback = null;
-+ this.read = null;
-+ this.write = null;
-+
-+ if (read != null) {
-+ return read.cancel(callback);
-+ }
-+ if (write != null) {
-+ return write.cancel(callback);
-+ }
-+
-+ // unreachable
-+ throw new InternalError();
-+ }
-+ }
-+
-+ private static final class CancellableReads implements Cancellable {
-+
-+ private Cancellable[] reads;
-+ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
-+
-+ private CancellableReads(final Cancellable[] reads) {
-+ this.reads = reads;
-+ }
-+
-+ @Override
-+ public boolean cancel() {
-+ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null);
-+
-+ if (reads == null) {
-+ return false;
-+ }
-+
-+ boolean ret = false;
-+
-+ for (final Cancellable read : reads) {
-+ ret |= read.cancel();
-+ }
-+
-+ return ret;
-+ }
-+ }
-+
-+ private static final class ChunkIOTask {
-+
-+ private final ServerLevel world;
-+ private final RegionDataController regionDataController;
-+ private final int chunkX;
-+ private final int chunkZ;
-+ private Priority priority;
-+ private PrioritisedExecutor.PrioritisedTask currentTask;
-+
-+ private final InProgressRead inProgressRead;
-+ private volatile InProgressWrite inProgressWrite;
-+ private final ReferenceOpenHashSet<InProgressWrite> allPendingWrites = new ReferenceOpenHashSet<>();
-+
-+ private RegionDataController.ReadData readData;
-+ private RegionDataController.WriteData writeData;
-+ private boolean failedWrite;
-+
-+ public ChunkIOTask(final ServerLevel world, final RegionDataController regionDataController,
-+ final int chunkX, final int chunkZ, final Priority priority, final InProgressRead inProgressRead) {
-+ this.world = world;
-+ this.regionDataController = regionDataController;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.priority = priority;
-+ this.inProgressRead = inProgressRead;
-+ }
-+
-+ public Priority getPriority() {
-+ synchronized (this) {
-+ return this.priority;
-+ }
-+ }
-+
-+ // must hold lock on this object
-+ private void updatePriority(final Priority priority) {
-+ this.priority = priority;
-+ if (this.currentTask != null) {
-+ this.currentTask.setPriority(priority);
-+ }
-+ for (final InProgressWrite write : this.allPendingWrites) {
-+ if (write.writeTask != null) {
-+ write.writeTask.setPriority(priority);
-+ }
-+ }
-+ }
-+
-+ public boolean setPriority(final Priority priority) {
-+ synchronized (this) {
-+ if (this.priority == priority) {
-+ return false;
-+ }
-+
-+ this.updatePriority(priority);
-+
-+ return true;
-+ }
-+ }
-+
-+ public boolean raisePriority(final Priority priority) {
-+ synchronized (this) {
-+ if (this.priority.isHigherOrEqualPriority(priority)) {
-+ return false;
-+ }
-+
-+ this.updatePriority(priority);
-+
-+ return true;
-+ }
-+ }
-+
-+ public boolean lowerPriority(final Priority priority) {
-+ synchronized (this) {
-+ if (this.priority.isLowerOrEqualPriority(priority)) {
-+ return false;
-+ }
-+
-+ this.updatePriority(priority);
-+
-+ return true;
-+ }
-+ }
-+
-+ private void pushPendingWrite(final InProgressWrite write) {
-+ this.inProgressWrite = write;
-+ synchronized (this) {
-+ this.allPendingWrites.add(write);
-+ if (write.writeTask != null) {
-+ write.writeTask.setPriority(this.priority);
-+ }
-+ }
-+ }
-+
-+ private void pendingWriteComplete(final InProgressWrite write) {
-+ synchronized (this) {
-+ this.allPendingWrites.remove(write);
-+ }
-+ }
-+
-+ public void scheduleReadIO() {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this) {
-+ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, this::performReadIO, this.priority);
-+ this.currentTask = task;
-+ }
-+ task.queue();
-+ }
-+
-+ private void performReadIO() {
-+ final InProgressRead read = this.inProgressRead;
-+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
-+
-+ final boolean[] canRead = new boolean[] { true };
-+
-+ if (read.hasNoWaiters()) {
-+ // cancelled read? go to task controller to confirm
-+ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkIOTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+
-+ if (!read.hasNoWaiters()) {
-+ return valueInMap;
-+ } else {
-+ canRead[0] = false;
-+ }
-+
-+ if (valueInMap.inProgressWrite != null) {
-+ return valueInMap;
-+ }
-+
-+ return null;
-+ });
-+
-+ if (inMap == null) {
-+ this.regionDataController.endTask(this);
-+ // read is cancelled - and no write pending, so we're done
-+ return;
-+ }
-+ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries -
-+ // the readers will just use the in progress write, so the value in canRead is good to use without
-+ // further synchronisation.
-+ }
-+
-+ if (canRead[0]) {
-+ RegionDataController.ReadData readData = null;
-+ Throwable throwable = null;
-+
-+ try {
-+ readData = this.regionDataController.readData(this.chunkX, this.chunkZ);
-+ } catch (final Throwable thr) {
-+ throwable = thr;
-+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
-+ }
-+
-+ if (throwable != null) {
-+ this.finishRead(null, throwable);
-+ } else {
-+ switch (readData.result()) {
-+ case NO_DATA:
-+ case SYNC_READ: {
-+ this.finishRead(readData.syncRead(), null);
-+ break;
-+ }
-+ case HAS_DATA: {
-+ this.readData = readData;
-+ this.scheduleReadDecompress();
-+ // read will handle write scheduling
-+ return;
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown state: " + readData.result());
-+ }
-+ }
-+ }
-+ }
-+
-+ if (!this.tryAbortWrite()) {
-+ this.scheduleWriteCompress();
-+ }
-+ }
-+
-+ private void scheduleReadDecompress() {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this) {
-+ task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority);
-+ this.currentTask = task;
-+ }
-+ task.queue();
-+ }
-+
-+ private void performReadDecompress() {
-+ final RegionDataController.ReadData readData = this.readData;
-+ this.readData = null;
-+
-+ CompoundTag compoundTag = null;
-+ Throwable throwable = null;
-+
-+ try {
-+ compoundTag = this.regionDataController.finishRead(this.chunkX, this.chunkZ, readData);
-+ } catch (final Throwable thr) {
-+ throwable = thr;
-+ LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr);
-+ }
-+
-+ if (compoundTag == null) {
-+ // need to re-try from the start
-+ this.scheduleReadIO();
-+ return;
-+ }
-+
-+ this.finishRead(compoundTag, throwable);
-+ if (!this.tryAbortWrite()) {
-+ this.scheduleWriteCompress();
-+ }
-+ }
-+
-+ private void finishRead(final CompoundTag compoundTag, final Throwable throwable) {
-+ this.inProgressRead.complete(this, compoundTag, throwable);
-+ }
-+
-+ public void scheduleWriteCompress() {
-+ final InProgressWrite inProgressWrite = this.inProgressWrite;
-+
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this) {
-+ task = this.regionDataController.compressionExecutor.createTask(() -> {
-+ ChunkIOTask.this.performWriteCompress(inProgressWrite);
-+ }, this.priority);
-+ this.currentTask = task;
-+ }
-+
-+ inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> {
-+ task.queue();
-+ });
-+ }
-+
-+ private boolean tryAbortWrite() {
-+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
-+ if (this.inProgressWrite == null) {
-+ final ChunkIOTask inMap = this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkIOTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+
-+ if (valueInMap.inProgressWrite != null) {
-+ return valueInMap;
-+ }
-+
-+ return null;
-+ });
-+
-+ if (inMap == null) {
-+ this.regionDataController.endTask(this);
-+ return true; // set the task value to null, indicating we're done
-+ } // else: inProgressWrite changed, so now we have something to write
-+ }
-+
-+ return false;
-+ }
-+
-+ private void performWriteCompress(final InProgressWrite inProgressWrite) {
-+ final CompoundTag write = inProgressWrite.value;
-+ if (!inProgressWrite.isComplete()) {
-+ throw new IllegalStateException("Should be writable");
-+ }
-+
-+ RegionDataController.WriteData writeData = null;
-+ boolean failedWrite = false;
-+
-+ try {
-+ writeData = this.regionDataController.startWrite(this.chunkX, this.chunkZ, write);
-+ } catch (final Throwable thr) {
-+ failedWrite = thr instanceof IOException;
-+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
-+ }
-+
-+ if (writeData == null) {
-+ // null if a throwable was encountered
-+
-+ // we cannot continue to the I/O stage here, so try to complete
-+
-+ if (this.tryCompleteWrite(inProgressWrite, failedWrite)) {
-+ return;
-+ } else {
-+ // fetch new data and try again
-+ this.scheduleWriteCompress();
-+ return;
-+ }
-+ } else {
-+ // writeData != null && !failedWrite
-+ // we can continue to I/O stage
-+ this.writeData = writeData;
-+ this.scheduleWriteIO(inProgressWrite);
-+ return;
-+ }
-+ }
-+
-+ private void scheduleWriteIO(final InProgressWrite inProgressWrite) {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this) {
-+ task = this.regionDataController.ioScheduler.createTask(this.chunkX, this.chunkZ, () -> {
-+ ChunkIOTask.this.runWriteIO(inProgressWrite);
-+ }, this.priority);
-+ this.currentTask = task;
-+ }
-+ task.queue();
-+ }
-+
-+ private void runWriteIO(final InProgressWrite inProgressWrite) {
-+ RegionDataController.WriteData writeData = this.writeData;
-+ this.writeData = null;
-+
-+ boolean failedWrite = false;
-+
-+ try {
-+ this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData);
-+ } catch (final Throwable thr) {
-+ failedWrite = thr instanceof IOException;
-+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
-+ }
-+
-+ if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) {
-+ // fetch new data and try again
-+ this.scheduleWriteCompress();
-+ }
-+ return;
-+ }
-+
-+ private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
-+
-+ final boolean[] done = new boolean[] { false };
-+
-+ this.regionDataController.chunkTasks.compute(chunkKey, (final long keyInMap, final ChunkIOTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkIOTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkIOTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkIOTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
-+ if (valueInMap.inProgressWrite == written) {
-+ valueInMap.failedWrite = failedWrite;
-+ done[0] = true;
-+ // keep the data in map if we failed the write so we can try to prevent data loss
-+ return failedWrite ? valueInMap : null;
-+ }
-+ // different data than expected, means we need to retry write
-+ return valueInMap;
-+ });
-+
-+ if (done[0]) {
-+ this.regionDataController.endTask(this);
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + ","
-+ + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode();
-+ }
-+
-+ private static final class InProgressRead {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
-+
-+ private CompoundTag value;
-+ private Throwable throwable;
-+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
-+
-+ public boolean hasNoWaiters() {
-+ return this.callbacks.isEmpty();
-+ }
-+
-+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.add(callback);
-+ }
-+
-+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.remove(callback);
-+ }
-+
-+ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) {
-+ this.value = value;
-+ this.throwable = throwable;
-+
-+ BiConsumer<CompoundTag, Throwable> consumer;
-+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
-+ try {
-+ consumer.accept(value == null ? null : value.copy(), throwable);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr);
-+ }
-+ }
-+ }
-+ }
-+
-+ private static final class InProgressWrite {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressWrite.class);
-+
-+ private CompoundTag value;
-+ private Throwable throwable;
-+ private volatile boolean complete;
-+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
-+
-+ private final PrioritisedExecutor.PrioritisedTask writeTask;
-+
-+ public InProgressWrite(final PrioritisedExecutor.PrioritisedTask writeTask) {
-+ this.writeTask = writeTask;
-+ }
-+
-+ public boolean isComplete() {
-+ return this.complete;
-+ }
-+
-+ public void schedule(final ChunkIOTask task, final Consumer<BiConsumer<CompoundTag, Throwable>> scheduler) {
-+ scheduler.accept((final CompoundTag data, final Throwable throwable) -> {
-+ InProgressWrite.this.complete(task, data, throwable);
-+ });
-+ }
-+
-+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.add(callback);
-+ }
-+
-+ public void addToWaiters(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer) {
-+ if (!this.callbacks.add(consumer)) {
-+ this.syncAccept(task, consumer, this.value, this.throwable);
-+ }
-+ }
-+
-+ private void syncAccept(final ChunkIOTask task, final BiConsumer<CompoundTag, Throwable> consumer, final CompoundTag value, final Throwable throwable) {
-+ try {
-+ consumer.accept(value == null ? null : value.copy(), throwable);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (write) for task " + task.toString(), thr);
-+ }
-+ }
-+
-+ public void complete(final ChunkIOTask task, final CompoundTag value, final Throwable throwable) {
-+ this.value = value;
-+ this.throwable = throwable;
-+ this.complete = true;
-+
-+ task.pendingWriteComplete(this);
-+
-+ BiConsumer<CompoundTag, Throwable> consumer;
-+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
-+ this.syncAccept(task, consumer, value, throwable);
-+ }
-+ }
-+
-+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.remove(callback);
-+ }
-+ }
-+ }
-+
-+ public static abstract class RegionDataController {
-+
-+ public final RegionFileType type;
-+ private final PrioritisedExecutor compressionExecutor;
-+ private final IOScheduler ioScheduler;
-+ private final ConcurrentLong2ReferenceChainedHashTable<ChunkIOTask> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
-+
-+ private final AtomicLong inProgressTasks = new AtomicLong();
-+
-+ public RegionDataController(final RegionFileType type, final PrioritisedExecutor ioExecutor,
-+ final PrioritisedExecutor compressionExecutor) {
-+ this.type = type;
-+ this.compressionExecutor = compressionExecutor;
-+ this.ioScheduler = new IOScheduler(ioExecutor);
-+ }
-+
-+ final void startTask(final ChunkIOTask task) {
-+ this.inProgressTasks.getAndIncrement();
-+ }
-+
-+ final void endTask(final ChunkIOTask task) {
-+ this.inProgressTasks.getAndDecrement();
-+ }
-+
-+ public boolean hasTasks() {
-+ return this.inProgressTasks.get() != 0L;
-+ }
-+
-+ public long getTotalWorkingTasks() {
-+ return this.inProgressTasks.get();
-+ }
-+
-+ public abstract RegionFileStorage getCache();
-+
-+ public static record WriteData(CompoundTag input, WriteResult result, DataOutputStream output, IORunnable write) {
-+ public static enum WriteResult {
-+ WRITE,
-+ DELETE;
-+ }
-+ }
-+
-+ public abstract WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException;
-+
-+ public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException;
-+
-+ public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) {
-+ public static enum ReadResult {
-+ NO_DATA,
-+ HAS_DATA,
-+ SYNC_READ;
-+ }
-+ }
-+
-+ public abstract ReadData readData(final int chunkX, final int chunkZ) throws IOException;
-+
-+ // if the return value is null, then the caller needs to re-try with a new call to readData()
-+ public abstract CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException;
-+
-+ public static interface IORunnable {
-+
-+ public void run(final RegionFile regionFile) throws IOException;
-+
-+ }
-+ }
-+
-+ private static final class IOScheduler {
-+
-+ private final ConcurrentLong2ReferenceChainedHashTable<RegionIOTasks> regionTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
-+ private final PrioritisedExecutor executor;
-+
-+ public IOScheduler(final PrioritisedExecutor executor) {
-+ this.executor = executor;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ,
-+ final Runnable run, final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask[] ret = new PrioritisedExecutor.PrioritisedTask[1];
-+ final long subOrder = this.executor.generateNextSubOrder();
-+ this.regionTasks.compute(CoordinateUtils.getChunkKey(chunkX >> REGION_FILE_SHIFT, chunkZ >> REGION_FILE_SHIFT),
-+ (final long regionKey, final RegionIOTasks existing) -> {
-+ final RegionIOTasks res;
-+ if (existing != null) {
-+ res = existing;
-+ } else {
-+ res = new RegionIOTasks(regionKey, IOScheduler.this);
-+ }
-+
-+ ret[0] = res.createTask(run, priority, subOrder);
-+
-+ return res;
-+ });
-+
-+ return ret[0];
-+ }
-+ }
-+
-+ private static final class RegionIOTasks implements Runnable {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(RegionIOTasks.class);
-+
-+ private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue();
-+ private final long regionKey;
-+ private final IOScheduler ioScheduler;
-+ private long createdTasks;
-+ private long executedTasks;
-+
-+ private PrioritisedExecutor.PrioritisedTask task;
-+
-+ public RegionIOTasks(final long regionKey, final IOScheduler ioScheduler) {
-+ this.regionKey = regionKey;
-+ this.ioScheduler = ioScheduler;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable run, final Priority priority,
-+ final long subOrder) {
-+ ++this.createdTasks;
-+ return new WrappedTask(this.queue.createTask(run, priority, subOrder));
-+ }
-+
-+ private void adjustTaskPriority() {
-+ final PrioritisedTaskQueue.PrioritySubOrderPair priority = this.queue.getHighestPrioritySubOrder();
-+ if (this.task == null) {
-+ if (priority == null) {
-+ return;
-+ }
-+ this.task = this.ioScheduler.executor.createTask(this, priority.priority(), priority.subOrder());
-+ this.task.queue();
-+ } else {
-+ if (priority == null) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.task.setPriorityAndSubOrder(priority.priority(), priority.subOrder());
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public void run() {
-+ final Runnable run;
-+ synchronized (this) {
-+ run = this.queue.pollTask();
-+ }
-+
-+ try {
-+ run.run();
-+ } finally {
-+ synchronized (this) {
-+ this.task = null;
-+ this.adjustTaskPriority();
-+ }
-+ this.ioScheduler.regionTasks.compute(this.regionKey, (final long keyInMap, final RegionIOTasks tasks) -> {
-+ if (tasks != RegionIOTasks.this) {
-+ throw new IllegalStateException("Region task mismatch");
-+ }
-+ ++tasks.executedTasks;
-+ if (tasks.createdTasks != tasks.executedTasks) {
-+ return tasks;
-+ }
-+
-+ if (tasks.task != null) {
-+ throw new IllegalStateException("Task may not be null when created==executed");
-+ }
-+
-+ return null;
-+ });
-+ }
-+ }
-+
-+ private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask {
-+
-+ private final PrioritisedExecutor.PrioritisedTask wrapped;
-+
-+ public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) {
-+ this.wrapped = wrap;
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor getExecutor() {
-+ return RegionIOTasks.this.ioScheduler.executor;
-+ }
-+
-+ @Override
-+ public boolean queue() {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.queue()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean isQueued() {
-+ return this.wrapped.isQueued();
-+ }
-+
-+ @Override
-+ public boolean cancel() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean execute() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.wrapped.getPriority();
-+ }
-+
-+ @Override
-+ public boolean setPriority(final Priority priority) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.setPriority(priority) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final Priority priority) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.raisePriority(priority) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean lowerPriority(final Priority priority) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public long getSubOrder() {
-+ return this.wrapped.getSubOrder();
-+ }
-+
-+ @Override
-+ public boolean setSubOrder(final long subOrder) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.setSubOrder(subOrder) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean raiseSubOrder(final long subOrder) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.raiseSubOrder(subOrder) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean lowerSubOrder(final long subOrder) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.lowerSubOrder(subOrder) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+
-+ @Override
-+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
-+ synchronized (RegionIOTasks.this) {
-+ if (this.wrapped.setPriorityAndSubOrder(priority, subOrder) && this.wrapped.isQueued()) {
-+ RegionIOTasks.this.adjustTaskPriority();
-+ return true;
-+ }
-+ return false;
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-@@ -0,0 +1,50 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+import java.io.IOException;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.concurrent.CompletionException;
-+
-+public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController {
-+
-+ private final ServerLevel world;
-+
-+ public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
-+ this.world = world;
-+ }
-+
-+ @Override
-+ public RegionFileStorage getCache() {
-+ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage();
-+ }
-+
-+ @Override
-+ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound);
-+ }
-+
-+ @Override
-+ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException {
-+ ((ChunkSystemChunkMap)this.world.getChunkSource().chunkMap).moonrise$writeFinishCallback(new ChunkPos(chunkX, chunkZ));
-+ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData);
-+ }
-+
-+ @Override
-+ public ReadData readData(final int chunkX, final int chunkZ) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-@@ -0,0 +1,73 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
-+import java.io.IOException;
-+import java.nio.file.Path;
-+
-+public final class EntityDataController extends MoonriseRegionFileIO.RegionDataController {
-+
-+ private final EntityRegionFileStorage storage;
-+
-+ public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
-+ this.storage = storage;
-+ }
-+
-+ @Override
-+ public RegionFileStorage getCache() {
-+ return this.storage;
-+ }
-+
-+ @Override
-+ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ checkPosition(new ChunkPos(chunkX, chunkZ), compound);
-+
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound);
-+ }
-+
-+ @Override
-+ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException {
-+ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData);
-+ }
-+
-+ @Override
-+ public ReadData readData(final int chunkX, final int chunkZ) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData);
-+ }
-+
-+ private static void checkPosition(final ChunkPos pos, final CompoundTag nbt) {
-+ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
-+ if (nbtPos != null && !pos.equals(nbtPos)) {
-+ throw new IllegalArgumentException(
-+ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
-+ + " but compound says coordinate is " + nbtPos
-+ );
-+ }
-+ }
-+
-+ public static final class EntityRegionFileStorage extends RegionFileStorage {
-+
-+ public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory,
-+ final boolean dsync) {
-+ super(regionStorageInfo, directory, dsync);
-+ }
-+
-+ @Override
-+ public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException {
-+ checkPosition(pos, nbt);
-+ super.write(pos, nbt);
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-@@ -0,0 +1,45 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+import java.io.IOException;
-+
-+public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController {
-+
-+ private final ServerLevel world;
-+
-+ public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
-+ this.world = world;
-+ }
-+
-+ @Override
-+ public RegionFileStorage getCache() {
-+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage();
-+ }
-+
-+ @Override
-+ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound);
-+ }
-+
-+ @Override
-+ public void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException {
-+ ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishWrite(chunkX, chunkZ, writeData);
-+ }
-+
-+ @Override
-+ public ReadData readData(final int chunkX, final int chunkZ) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$readData(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ public CompoundTag finishRead(final int chunkX, final int chunkZ, final ReadData readData) throws IOException {
-+ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$finishRead(chunkX, chunkZ, readData);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
-@@ -0,0 +1,10 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level;
-+
-+import net.minecraft.world.level.ChunkPos;
-+import java.io.IOException;
-+
-+public interface ChunkSystemChunkMap {
-+
-+ public void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException;
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-@@ -0,0 +1,33 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+
-+public interface ChunkSystemLevel {
-+
-+ public EntityLookup moonrise$getEntityLookup();
-+
-+ public void moonrise$setEntityLookup(final EntityLookup entityLookup);
-+
-+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
-+
-+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ);
-+
-+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus);
-+
-+ public void moonrise$midTickTasks();
-+
-+ public ChunkData moonrise$getChunkData(final long chunkKey);
-+
-+ public ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ);
-+
-+ public ChunkData moonrise$requestChunkData(final long chunkKey);
-+
-+ public ChunkData moonrise$releaseChunkData(final long chunkKey);
-+
-+ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
-@@ -0,0 +1,10 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level;
-+
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+
-+public interface ChunkSystemLevelReader {
-+
-+ public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c278f8ef806f0b45c28cc3040c7db052cb51e053
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-@@ -0,0 +1,62 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level;
-+
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.list.ReferenceList;
-+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+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.ChunkHolder;
-+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;
-+import java.util.function.Consumer;
-+
-+public interface ChunkSystemServerLevel extends ChunkSystemLevel {
-+
-+ public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
-+
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController();
-+
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController();
-+
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController();
-+
-+ public int moonrise$getRegionChunkShift();
-+
-+ // Paper
-+
-+ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
-+
-+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final Priority priority,
-+ final Consumer<List<ChunkAccess>> onLoad);
-+
-+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final ChunkStatus chunkStatus, final Priority priority,
-+ final Consumer<List<ChunkAccess>> onLoad);
-+
-+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
-+ final Priority priority,
-+ final Consumer<List<ChunkAccess>> onLoad);
-+
-+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
-+ final ChunkStatus chunkStatus, final Priority priority,
-+ final Consumer<List<ChunkAccess>> onLoad);
-+
-+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
-+
-+ public long moonrise$getLastMidTickFailure();
-+
-+ 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/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
-@@ -0,0 +1,21 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
-+
-+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
-+
-+public final class ChunkData {
-+
-+ private int referenceCount = 0;
-+ public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players
-+
-+ public ChunkData() {
-+
-+ }
-+
-+ public int increaseRef() {
-+ return ++this.referenceCount;
-+ }
-+
-+ public int decreaseRef() {
-+ return --this.referenceCount;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
-@@ -0,0 +1,26 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import java.util.List;
-+
-+public interface ChunkSystemChunkHolder {
-+
-+ public NewChunkHolder moonrise$getRealChunkHolder();
-+
-+ public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder);
-+
-+ public void moonrise$addReceivedChunk(final ServerPlayer player);
-+
-+ public void moonrise$removeReceivedChunk(final ServerPlayer player);
-+
-+ public boolean moonrise$hasChunkBeenSent();
-+
-+ public boolean moonrise$hasChunkBeenSent(final ServerPlayer to);
-+
-+ public List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge);
-+
-+ public LevelChunk moonrise$getFullChunk();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
-@@ -0,0 +1,26 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
-+
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+
-+public interface ChunkSystemChunkStatus {
-+
-+ public boolean moonrise$isParallelCapable();
-+
-+ public void moonrise$setParallelCapable(final boolean value);
-+
-+ public int moonrise$getWriteRadius();
-+
-+ public void moonrise$setWriteRadius(final int value);
-+
-+ public ChunkStatus moonrise$getNextStatus();
-+
-+ public boolean moonrise$isEmptyLoadStatus();
-+
-+ public void moonrise$setEmptyLoadStatus(final boolean value);
-+
-+ public boolean moonrise$isEmptyGenStatus();
-+
-+ public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
-+import net.minecraft.server.level.ChunkMap;
-+
-+public interface ChunkSystemDistanceManager {
-+
-+ public ChunkMap moonrise$getChunkMap();
-+
-+ public ChunkHolderManager moonrise$getChunkHolderManager();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896dd0d5febc
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
-@@ -0,0 +1,13 @@
-+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/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7aea4e343581b977d11af90f9f65eac3532eade1
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-@@ -0,0 +1,569 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.common.list.EntityList;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
-+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
-+import com.google.common.collect.ImmutableList;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.nbt.NbtUtils;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.EntitySpawnReason;
-+import net.minecraft.world.entity.EntityType;
-+import net.minecraft.world.entity.boss.EnderDragonPart;
-+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.storage.EntityStorage;
-+import net.minecraft.world.level.entity.Visibility;
-+import net.minecraft.world.phys.AABB;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.function.Predicate;
-+
-+public final class ChunkEntitySlices {
-+
-+ public final int minSection;
-+ public final int maxSection;
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final Level world;
-+
-+ private final EntityCollectionBySection allEntities;
-+ private final EntityCollectionBySection hardCollidingEntities;
-+ private final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
-+ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType;
-+ private final EntityList entities = new EntityList();
-+
-+ public FullChunkStatus status;
-+ public final ChunkData chunkData;
-+
-+ private boolean isTransient;
-+
-+ public boolean isTransient() {
-+ return this.isTransient;
-+ }
-+
-+ public void setTransient(final boolean value) {
-+ this.isTransient = value;
-+ }
-+
-+ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
-+ final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive
-+ this.minSection = minSection;
-+ this.maxSection = maxSection;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.world = world;
-+
-+ this.allEntities = new EntityCollectionBySection(this);
-+ this.hardCollidingEntities = new EntityCollectionBySection(this);
-+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
-+ this.entitiesByType = new Reference2ObjectOpenHashMap<>();
-+
-+ this.status = status;
-+ this.chunkData = chunkData;
-+ }
-+
-+ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) {
-+ // TODO check this and below on update for format changes
-+ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList());
-+ }
-+
-+ // Paper start - rewrite chunk system
-+ public static void copyEntities(final CompoundTag from, final CompoundTag into) {
-+ if (from == null) {
-+ return;
-+ }
-+ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND);
-+ if (entitiesFrom == null || entitiesFrom.isEmpty()) {
-+ return;
-+ }
-+
-+ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND);
-+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities
-+ entitiesInto.addAll(0, entitiesFrom);
-+ }
-+
-+ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) {
-+ return saveEntityChunk0(entities, chunkPos, world, false);
-+ }
-+
-+ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) {
-+ if (!force && entities.isEmpty()) {
-+ return null;
-+ }
-+
-+ final ListTag entitiesTag = new ListTag();
-+ for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) {
-+ CompoundTag compoundTag = new CompoundTag();
-+ if (entity.save(compoundTag)) {
-+ entitiesTag.add(compoundTag);
-+ }
-+ }
-+ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag());
-+ ret.put("Entities", entitiesTag);
-+ EntityStorage.writeChunkPos(ret, chunkPos);
-+
-+ return !force && entitiesTag.isEmpty() ? null : ret;
-+ }
-+
-+ public CompoundTag save() {
-+ final int len = this.entities.size();
-+ if (len == 0) {
-+ return null;
-+ }
-+
-+ final Entity[] rawData = this.entities.getRawData();
-+ final List<Entity> collectedEntities = new ArrayList<>(len);
-+ for (int i = 0; i < len; ++i) {
-+ final Entity entity = rawData[i];
-+ if (entity.shouldBeSaved()) {
-+ collectedEntities.add(entity);
-+ }
-+ }
-+
-+ if (collectedEntities.isEmpty()) {
-+ return null;
-+ }
-+
-+ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world);
-+ }
-+
-+ // returns true if this chunk has transient entities remaining
-+ public boolean unload() {
-+ final int len = this.entities.size();
-+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
-+
-+ for (int i = 0; i < len; ++i) {
-+ final Entity entity = collectedEntities[i];
-+ if (entity.isRemoved()) {
-+ // removed by us below
-+ continue;
-+ }
-+ if (entity.shouldBeSaved()) {
-+ PlatformHooks.get().unloadEntity(entity);
-+ if (entity.isVehicle()) {
-+ // we cannot assume that these entities are contained within this chunk, because entities can
-+ // desync - so we need to remove them all
-+ for (final Entity passenger : entity.getIndirectPassengers()) {
-+ PlatformHooks.get().unloadEntity(passenger);
-+ }
-+ }
-+ }
-+ }
-+
-+ return this.entities.size() != 0;
-+ }
-+
-+ public List<Entity> getAllEntities() {
-+ final int len = this.entities.size();
-+ if (len == 0) {
-+ return new ArrayList<>();
-+ }
-+
-+ final Entity[] rawData = this.entities.getRawData();
-+ final List<Entity> collectedEntities = new ArrayList<>(len);
-+ for (int i = 0; i < len; ++i) {
-+ collectedEntities.add(rawData[i]);
-+ }
-+
-+ return collectedEntities;
-+ }
-+
-+ public boolean isEmpty() {
-+ return this.entities.size() == 0;
-+ }
-+
-+ public void mergeInto(final ChunkEntitySlices slices) {
-+ final Entity[] entities = this.entities.getRawData();
-+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
-+ final Entity entity = entities[i];
-+ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY());
-+ }
-+ }
-+
-+ private boolean preventStatusUpdates;
-+ public boolean startPreventingStatusUpdates() {
-+ final boolean ret = this.preventStatusUpdates;
-+ this.preventStatusUpdates = true;
-+ return ret;
-+ }
-+
-+ public boolean isPreventingStatusUpdates() {
-+ return this.preventStatusUpdates;
-+ }
-+
-+ public void stopPreventingStatusUpdates(final boolean prev) {
-+ this.preventStatusUpdates = prev;
-+ }
-+
-+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) {
-+ this.status = status;
-+
-+ final Entity[] entities = this.entities.getRawData();
-+
-+ for (int i = 0, size = this.entities.size(); i < size; ++i) {
-+ final Entity entity = entities[i];
-+
-+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity);
-+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status);
-+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity);
-+
-+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false);
-+ }
-+ }
-+
-+ public boolean addEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.add(entity)) {
-+ return false;
-+ }
-+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status);
-+ ((ChunkSystemEntity)entity).moonrise$setChunkData(this.chunkData);
-+ final int sectionIndex = chunkSection - this.minSection;
-+
-+ this.allEntities.addEntity(entity, sectionIndex);
-+
-+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+ this.hardCollidingEntities.addEntity(entity, sectionIndex);
-+ }
-+
-+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
-+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
-+
-+ if (entry.getKey().isInstance(entity)) {
-+ entry.getValue().addEntity(entity, sectionIndex);
-+ }
-+ }
-+
-+ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
-+ if (byType != null) {
-+ byType.addEntity(entity, sectionIndex);
-+ } else {
-+ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this));
-+ byType.addEntity(entity, sectionIndex);
-+ }
-+
-+ return true;
-+ }
-+
-+ public boolean removeEntity(final Entity entity, final int chunkSection) {
-+ if (!this.entities.remove(entity)) {
-+ return false;
-+ }
-+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
-+ ((ChunkSystemEntity)entity).moonrise$setChunkData(null);
-+ final int sectionIndex = chunkSection - this.minSection;
-+
-+ this.allEntities.removeEntity(entity, sectionIndex);
-+
-+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
-+ }
-+
-+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
-+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
-+
-+ if (entry.getKey().isInstance(entity)) {
-+ entry.getValue().removeEntity(entity, sectionIndex);
-+ }
-+ }
-+
-+ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
-+ byType.removeEntity(entity, sectionIndex);
-+
-+ return true;
-+ }
-+
-+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
-+ }
-+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ this.allEntities.getEntities(except, box, into, predicate);
-+ }
-+
-+
-+ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
-+ final int maxCount) {
-+ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount);
-+ }
-+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
-+
-+ if (byType != null) {
-+ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate);
-+ }
-+ }
-+
-+ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate, final int maxCount) {
-+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
-+
-+ if (byType != null) {
-+ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount);
-+ }
-+
-+ return false;
-+ }
-+
-+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
-+ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
-+
-+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
-+ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
-+ if (sectionEntities == null) {
-+ continue;
-+ }
-+
-+ final Entity[] storage = sectionEntities.storage;
-+
-+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
-+
-+ if (clazz.isInstance(entity)) {
-+ ret.addEntity(entity, sectionIndex);
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
-+ if (collection != null) {
-+ collection.getEntities(except, box, (List)into, (Predicate)predicate);
-+ } else {
-+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
-+ collection.getEntities(except, box, (List)into, (Predicate)predicate);
-+ }
-+ }
-+
-+ public <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate, final int maxCount) {
-+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
-+ if (collection != null) {
-+ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount);
-+ } else {
-+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
-+ return collection.getEntitiesLimited(except, box, (List)into, (Predicate)predicate, maxCount);
-+ }
-+ }
-+
-+ private static final class BasicEntityList<E extends Entity> {
-+
-+ private static final Entity[] EMPTY = new Entity[0];
-+ private static final int DEFAULT_CAPACITY = 4;
-+
-+ private E[] storage;
-+ private int size;
-+
-+ public BasicEntityList() {
-+ this(0);
-+ }
-+
-+ public BasicEntityList(final int cap) {
-+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
-+ }
-+
-+ public boolean isEmpty() {
-+ return this.size == 0;
-+ }
-+
-+ public int size() {
-+ return this.size;
-+ }
-+
-+ private void resize() {
-+ if (this.storage == EMPTY) {
-+ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
-+ } else {
-+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
-+ }
-+ }
-+
-+ public void add(final E entity) {
-+ final int idx = this.size++;
-+ if (idx >= this.storage.length) {
-+ this.resize();
-+ this.storage[idx] = entity;
-+ } else {
-+ this.storage[idx] = entity;
-+ }
-+ }
-+
-+ public int indexOf(final E entity) {
-+ final E[] storage = this.storage;
-+
-+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
-+ if (storage[i] == entity) {
-+ return i;
-+ }
-+ }
-+
-+ return -1;
-+ }
-+
-+ public boolean remove(final E entity) {
-+ final int idx = this.indexOf(entity);
-+ if (idx == -1) {
-+ return false;
-+ }
-+
-+ final int size = --this.size;
-+ final E[] storage = this.storage;
-+ if (idx != size) {
-+ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
-+ }
-+
-+ storage[size] = null;
-+
-+ return true;
-+ }
-+
-+ public boolean has(final E entity) {
-+ return this.indexOf(entity) != -1;
-+ }
-+ }
-+
-+ private static final class EntityCollectionBySection {
-+
-+ private final ChunkEntitySlices slices;
-+ private final BasicEntityList<Entity>[] entitiesBySection;
-+ private int count;
-+
-+ public EntityCollectionBySection(final ChunkEntitySlices slices) {
-+ this.slices = slices;
-+
-+ final int sectionCount = slices.maxSection - slices.minSection + 1;
-+
-+ this.entitiesBySection = new BasicEntityList[sectionCount];
-+ }
-+
-+ public void addEntity(final Entity entity, final int sectionIndex) {
-+ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
-+
-+ if (list != null && list.has(entity)) {
-+ return;
-+ }
-+
-+ if (list == null) {
-+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
-+ }
-+
-+ list.add(entity);
-+ ++this.count;
-+ }
-+
-+ public void removeEntity(final Entity entity, final int sectionIndex) {
-+ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
-+
-+ if (list == null || !list.remove(entity)) {
-+ return;
-+ }
-+
-+ --this.count;
-+
-+ if (list.isEmpty()) {
-+ this.entitiesBySection[sectionIndex] = null;
-+ }
-+ }
-+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ if (this.count == 0) {
-+ return;
-+ }
-+
-+ final int minSection = this.slices.minSection;
-+ final int maxSection = this.slices.maxSection;
-+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
-+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
-+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
-+
-+ if (list == null) {
-+ continue;
-+ }
-+
-+ final Entity[] storage = list.storage;
-+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
-+
-+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
-+ continue;
-+ }
-+
-+ if (predicate != null && !predicate.test(entity)) {
-+ continue;
-+ }
-+
-+ into.add(entity);
-+ }
-+ }
-+ }
-+
-+ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
-+ final int maxCount) {
-+ if (this.count == 0) {
-+ return false;
-+ }
-+
-+ final int minSection = this.slices.minSection;
-+ final int maxSection = this.slices.maxSection;
-+
-+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
-+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
-+
-+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
-+
-+ for (int section = min; section <= max; ++section) {
-+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
-+
-+ if (list == null) {
-+ continue;
-+ }
-+
-+ final Entity[] storage = list.storage;
-+
-+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
-+ final Entity entity = storage[i];
-+
-+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
-+ continue;
-+ }
-+
-+ if (predicate != null && !predicate.test(entity)) {
-+ continue;
-+ }
-+
-+ into.add(entity);
-+ if (into.size() >= maxCount) {
-+ return true;
-+ }
-+ }
-+ }
-+
-+ return false;
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7554c109c35397bc1a43dd80e87764fd78645bbf
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-@@ -0,0 +1,1002 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
-+
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
-+import ca.spottedleaf.moonrise.common.list.EntityList;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.util.AbortableIterationConsumer;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.EntityType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.entity.EntityInLevelCallback;
-+import net.minecraft.world.level.entity.EntityTypeTest;
-+import net.minecraft.world.level.entity.LevelCallback;
-+import net.minecraft.world.level.entity.LevelEntityGetter;
-+import net.minecraft.world.level.entity.Visibility;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.NoSuchElementException;
-+import java.util.Objects;
-+import java.util.UUID;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.function.Consumer;
-+import java.util.function.Predicate;
-+
-+public abstract class EntityLookup implements LevelEntityGetter<Entity> {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class);
-+
-+ protected static final int REGION_SHIFT = 5;
-+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
-+ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
-+
-+ public final Level world;
-+
-+ protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
-+
-+ protected final LevelCallback<Entity> worldCallback;
-+
-+ protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
-+ protected final ConcurrentHashMap<UUID, Entity> entityByUUID = new ConcurrentHashMap<>();
-+ protected final EntityList accessibleEntities = new EntityList();
-+
-+ public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
-+ this.world = world;
-+ this.worldCallback = worldCallback;
-+ }
-+
-+ protected abstract Boolean blockTicketUpdates();
-+
-+ protected abstract void setBlockTicketUpdates(final Boolean value);
-+
-+ protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason);
-+
-+ protected abstract void checkThread(final Entity entity, final String reason);
-+
-+ protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk);
-+
-+ protected abstract void onEmptySlices(final int chunkX, final int chunkZ);
-+
-+ protected abstract void entitySectionChangeCallback(
-+ final Entity entity,
-+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
-+ final int newSectionX, final int newSectionY, final int newSectionZ
-+ );
-+
-+ protected abstract void addEntityCallback(final Entity entity);
-+
-+ protected abstract void removeEntityCallback(final Entity entity);
-+
-+ protected abstract void entityStartLoaded(final Entity entity);
-+
-+ protected abstract void entityEndLoaded(final Entity entity);
-+
-+ protected abstract void entityStartTicking(final Entity entity);
-+
-+ protected abstract void entityEndTicking(final Entity entity);
-+
-+ protected abstract boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event);
-+
-+ private static Entity maskNonAccessible(final Entity entity) {
-+ if (entity == null) {
-+ return null;
-+ }
-+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
-+ return visibility.isAccessible() ? entity : null;
-+ }
-+
-+ @Override
-+ public Entity get(final int id) {
-+ return maskNonAccessible(this.entityById.get((long)id));
-+ }
-+
-+ @Override
-+ public Entity get(final UUID id) {
-+ return maskNonAccessible(id == null ? null : this.entityByUUID.get(id));
-+ }
-+
-+ public boolean hasEntity(final UUID uuid) {
-+ return this.get(uuid) != null;
-+ }
-+
-+ public String getDebugInfo() {
-+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size();
-+ }
-+
-+ protected static final class ArrayIterable<T> implements Iterable<T> {
-+
-+ private final T[] array;
-+ private final int off;
-+ private final int length;
-+
-+ public ArrayIterable(final T[] array, final int off, final int length) {
-+ this.array = array;
-+ this.off = off;
-+ this.length = length;
-+ if (length > array.length) {
-+ throw new IllegalArgumentException("Length must be no greater-than the array length");
-+ }
-+ }
-+
-+ @Override
-+ public Iterator<T> iterator() {
-+ return new ArrayIterator<>(this.array, this.off, this.length);
-+ }
-+
-+ protected static final class ArrayIterator<T> implements Iterator<T> {
-+
-+ private final T[] array;
-+ private int off;
-+ private final int length;
-+
-+ public ArrayIterator(final T[] array, final int off, final int length) {
-+ this.array = array;
-+ this.off = off;
-+ this.length = length;
-+ }
-+
-+ @Override
-+ public boolean hasNext() {
-+ return this.off < this.length;
-+ }
-+
-+ @Override
-+ public T next() {
-+ if (this.off >= this.length) {
-+ throw new NoSuchElementException();
-+ }
-+ return this.array[this.off++];
-+ }
-+
-+ @Override
-+ public void remove() {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public Iterable<Entity> getAll() {
-+ synchronized (this.accessibleEntities) {
-+ final int len = this.accessibleEntities.size();
-+ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class);
-+
-+ Objects.checkFromToIndex(0, len, cpy.length);
-+
-+ return new ArrayIterable<>(cpy, 0, len);
-+ }
-+ }
-+
-+ public int getEntityCount() {
-+ synchronized (this.accessibleEntities) {
-+ return this.accessibleEntities.size();
-+ }
-+ }
-+
-+ public Entity[] getAllCopy() {
-+ synchronized (this.accessibleEntities) {
-+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
-+ }
-+ }
-+
-+ @Override
-+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) {
-+ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) {
-+ final Entity entity = iterator.next();
-+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
-+ if (!visibility.isAccessible()) {
-+ continue;
-+ }
-+ final U casted = filter.tryCast(entity);
-+ if (casted != null && action.accept(casted).shouldAbort()) {
-+ break;
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public void get(final AABB box, final Consumer<Entity> action) {
-+ List<Entity> entities = new ArrayList<>();
-+ this.getEntities((Entity)null, box, entities, null);
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ action.accept(entities.get(i));
-+ }
-+ }
-+
-+ @Override
-+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) {
-+ List<Entity> entities = new ArrayList<>();
-+ this.getEntities((Entity)null, box, entities, null);
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final U casted = filter.tryCast(entities.get(i));
-+ if (casted != null && action.accept(casted).shouldAbort()) {
-+ break;
-+ }
-+ }
-+ }
-+
-+ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved,
-+ final boolean created, final boolean destroyed) {
-+ this.checkThread(entity, "Entity status change must only happen on the main thread");
-+
-+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
-+ // recursive status update
-+ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable());
-+ return;
-+ }
-+
-+ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
-+
-+ if (entityStatusUpdateBefore) {
-+ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
-+ return;
-+ }
-+
-+ try {
-+ final Boolean ticketBlockBefore = this.blockTicketUpdates();
-+ try {
-+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true);
-+ try {
-+ if (created) {
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onCreated(entity);
-+ }
-+ }
-+
-+ if (oldVisibility == newVisibility) {
-+ if (moved && newVisibility.isAccessible()) {
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onSectionChange(entity);
-+ }
-+ }
-+ return;
-+ }
-+
-+ if (newVisibility.ordinal() > oldVisibility.ordinal()) {
-+ // status upgrade
-+ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
-+ EntityLookup.this.entityStartLoaded(entity);
-+ synchronized (this.accessibleEntities) {
-+ this.accessibleEntities.add(entity);
-+ }
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onTrackingStart(entity);
-+ }
-+ }
-+
-+ if (!oldVisibility.isTicking() && newVisibility.isTicking()) {
-+ EntityLookup.this.entityStartTicking(entity);
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onTickingStart(entity);
-+ }
-+ }
-+ } else {
-+ // status downgrade
-+ if (oldVisibility.isTicking() && !newVisibility.isTicking()) {
-+ EntityLookup.this.entityEndTicking(entity);
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onTickingEnd(entity);
-+ }
-+ }
-+
-+ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
-+ EntityLookup.this.entityEndLoaded(entity);
-+ synchronized (this.accessibleEntities) {
-+ this.accessibleEntities.remove(entity);
-+ }
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onTrackingEnd(entity);
-+ }
-+ }
-+ }
-+
-+ if (moved && newVisibility.isAccessible()) {
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onSectionChange(entity);
-+ }
-+ }
-+
-+ if (destroyed) {
-+ if (EntityLookup.this.worldCallback != null) {
-+ EntityLookup.this.worldCallback.onDestroyed(entity);
-+ }
-+ }
-+ } finally {
-+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false);
-+ }
-+ } finally {
-+ this.setBlockTicketUpdates(ticketBlockBefore);
-+ }
-+ } finally {
-+ if (slices != null) {
-+ slices.stopPreventingStatusUpdates(false);
-+ }
-+ }
-+ }
-+
-+ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
-+ this.getChunk(x, z).updateStatus(newStatus, this);
-+ }
-+
-+ public void addLegacyChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, true);
-+ }
-+
-+ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, true);
-+ }
-+
-+ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
-+ this.addEntityChunk(entities, forChunk, false);
-+ }
-+
-+ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
-+ if (!this.addEntity(root, fromDisk, true)) {
-+ // possible we are a passenger, and so should dismount from any valid entity in the world
-+ root.stopRiding();
-+ return;
-+ }
-+ for (final Entity passenger : root.getPassengers()) {
-+ this.addRecursivelySafe(passenger, fromDisk);
-+ }
-+ }
-+
-+ protected void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) {
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity entity = entities.get(i);
-+ if (entity.isPassenger()) {
-+ continue;
-+ }
-+
-+ if (forChunk != null && !entity.chunkPosition().equals(forChunk)) {
-+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk);
-+ // can't set removed here, as we may not own the chunk position
-+ // skip the entity
-+ continue;
-+ }
-+
-+ final Vec3 rootPosition = entity.position();
-+
-+ // always adjust positions before adding passengers in case plugins access the entity, and so that
-+ // they are added to the right entity chunk
-+ for (final Entity passenger : entity.getIndirectPassengers()) {
-+ if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) {
-+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z);
-+ }
-+ }
-+
-+ this.addRecursivelySafe(entity, fromDisk);
-+ }
-+ }
-+
-+ public boolean addNewEntity(final Entity entity) {
-+ return this.addNewEntity(entity, true);
-+ }
-+
-+ public boolean addNewEntity(final Entity entity, final boolean event) {
-+ return this.addEntity(entity, false, event);
-+ }
-+
-+ public static Visibility getEntityStatus(final Entity entity) {
-+ if (entity.isAlwaysTicking()) {
-+ return Visibility.TICKING;
-+ }
-+ final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus();
-+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
-+ }
-+
-+ protected boolean addEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+ final BlockPos pos = entity.blockPosition();
-+ final int sectionX = pos.getX() >> 4;
-+ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
-+ final int sectionZ = pos.getZ() >> 4;
-+ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread");
-+
-+ if (entity.isRemoved()) {
-+ LOGGER.warn("Refusing to add removed entity: " + entity);
-+ return false;
-+ }
-+
-+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
-+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable());
-+ return false;
-+ }
-+
-+ if (!this.screenEntity(entity, fromDisk, event)) {
-+ return false;
-+ }
-+
-+ Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity);
-+ if (currentlyMapped != null) {
-+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity);
-+ return false;
-+ }
-+
-+ currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity);
-+ if (currentlyMapped != null) {
-+ // need to remove mapping for id
-+ this.entityById.remove((long)entity.getId(), entity);
-+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity);
-+ return false;
-+ }
-+
-+ ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ);
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
-+ if (!slices.addEntity(entity, sectionY)) {
-+ LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")");
-+ }
-+
-+ entity.setLevelCallback(new EntityCallback(entity));
-+
-+ this.addEntityCallback(entity);
-+
-+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
-+
-+ return true;
-+ }
-+
-+ public boolean canRemoveEntity(final Entity entity) {
-+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
-+ return false;
-+ }
-+
-+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
-+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
-+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
-+ return slices == null || !slices.isPreventingStatusUpdates();
-+ }
-+
-+ protected void removeEntity(final Entity entity) {
-+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
-+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
-+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
-+ this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main");
-+ if (!entity.isRemoved()) {
-+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity");
-+ }
-+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
-+ // all entities should be in a chunk
-+ if (slices == null) {
-+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")");
-+ } else {
-+ if (slices.isPreventingStatusUpdates()) {
-+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates");
-+ }
-+ if (!slices.removeEntity(entity, sectionY)) {
-+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
-+ }
-+ }
-+ ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE);
-+
-+
-+ Entity currentlyMapped;
-+ if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) {
-+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped);
-+ }
-+
-+ Entity[] currentlyMappedArr = new Entity[1];
-+
-+ // need reference equality
-+ this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> {
-+ currentlyMappedArr[0] = valueInMap;
-+ if (valueInMap != entity) {
-+ return valueInMap;
-+ }
-+ return null;
-+ });
-+
-+ if (currentlyMappedArr[0] != entity) {
-+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]);
-+ }
-+
-+ if (slices != null && slices.isEmpty()) {
-+ this.onEmptySlices(sectionX, sectionZ);
-+ }
-+ }
-+
-+ protected ChunkEntitySlices moveEntity(final Entity entity) {
-+ // ensure we own the entity
-+ this.checkThread(entity, "Cannot move entity off-main");
-+
-+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
-+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
-+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
-+ final BlockPos newPos = entity.blockPosition();
-+ final int newSectionX = newPos.getX() >> 4;
-+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
-+ final int newSectionZ = newPos.getZ() >> 4;
-+
-+ if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) {
-+ return null;
-+ }
-+
-+ // ensure the new section is owned by this tick thread
-+ this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main");
-+
-+ // ensure the old section is owned by this tick thread
-+ this.checkThread(sectionX, sectionZ, "Cannot move entity off-main");
-+
-+ final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ);
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
-+
-+ if (!old.removeEntity(entity, sectionY)) {
-+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section");
-+ }
-+
-+ if (!slices.addEntity(entity, newSectionY)) {
-+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section");
-+ }
-+
-+ ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY);
-+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ);
-+
-+ if (old.isEmpty()) {
-+ this.onEmptySlices(sectionX, sectionZ);
-+ }
-+
-+ this.entitySectionChangeCallback(
-+ entity,
-+ sectionX, sectionY, sectionZ,
-+ newSectionX, newSectionY, newSectionZ
-+ );
-+
-+ return slices;
-+ }
-+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ chunk.getEntities(except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ chunk.getHardCollidingEntities(except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ chunk.getEntities(clazz, except, box, into, predicate);
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ //////// Limited ////////
-+
-+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
-+ final int maxCount) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ if (chunk.getEntities(except, box, into, predicate, maxCount)) {
-+ return;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate, final int maxCount) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) {
-+ return;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
-+ final Predicate<? super T> predicate, final int maxCount) {
-+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
-+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
-+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
-+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
-+
-+ final int minRegionX = minChunkX >> REGION_SHIFT;
-+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
-+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
-+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
-+
-+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
-+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
-+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
-+
-+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
-+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
-+
-+ if (region == null) {
-+ continue;
-+ }
-+
-+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
-+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
-+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
-+ continue;
-+ }
-+
-+ if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) {
-+ return;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
-+ this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main");
-+ synchronized (this) {
-+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ);
-+ if (curr != null) {
-+ this.removeChunk(chunkX, chunkZ);
-+
-+ curr.mergeInto(slices);
-+
-+ this.addChunk(chunkX, chunkZ, slices);
-+ } else {
-+ this.addChunk(chunkX, chunkZ, slices);
-+ }
-+ }
-+ }
-+
-+ public void entitySectionUnload(final int chunkX, final int chunkZ) {
-+ this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main");
-+ this.removeChunk(chunkX, chunkZ);
-+ }
-+
-+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
-+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ if (region == null) {
-+ return null;
-+ }
-+
-+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
-+ }
-+
-+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
-+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ final ChunkEntitySlices ret;
-+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
-+ return this.createEntityChunk(chunkX, chunkZ, true);
-+ }
-+
-+ return ret;
-+ }
-+
-+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
-+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
-+
-+ return this.regions.get(key);
-+ }
-+
-+ protected synchronized void removeChunk(final int chunkX, final int chunkZ) {
-+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
-+
-+ final ChunkSlicesRegion region = this.regions.get(key);
-+ final int remaining = region.remove(relIndex);
-+
-+ if (remaining == 0) {
-+ this.regions.remove(key);
-+ }
-+ }
-+
-+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
-+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
-+
-+ ChunkSlicesRegion region = this.regions.get(key);
-+ if (region != null) {
-+ region.add(relIndex, slices);
-+ } else {
-+ region = new ChunkSlicesRegion();
-+ region.add(relIndex, slices);
-+ this.regions.put(key, region);
-+ }
-+ }
-+
-+ public static final class ChunkSlicesRegion {
-+
-+ private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
-+ private int sliceCount;
-+
-+ public ChunkEntitySlices get(final int index) {
-+ return this.slices[index];
-+ }
-+
-+ public int remove(final int index) {
-+ final ChunkEntitySlices slices = this.slices[index];
-+ if (slices == null) {
-+ throw new IllegalStateException();
-+ }
-+
-+ this.slices[index] = null;
-+
-+ return --this.sliceCount;
-+ }
-+
-+ public void add(final int index, final ChunkEntitySlices slices) {
-+ final ChunkEntitySlices curr = this.slices[index];
-+ if (curr != null) {
-+ throw new IllegalStateException();
-+ }
-+
-+ this.slices[index] = slices;
-+
-+ ++this.sliceCount;
-+ }
-+ }
-+
-+ protected final class EntityCallback implements EntityInLevelCallback {
-+
-+ public final Entity entity;
-+
-+ public EntityCallback(final Entity entity) {
-+ this.entity = entity;
-+ }
-+
-+ @Override
-+ public void onMove() {
-+ final Entity entity = this.entity;
-+ final Visibility oldVisibility = getEntityStatus(entity);
-+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
-+ if (newSlices == null) {
-+ // no new section, so didn't change sections
-+ return;
-+ }
-+
-+ final Visibility newVisibility = getEntityStatus(entity);
-+
-+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
-+ }
-+
-+ @Override
-+ public void onRemove(final Entity.RemovalReason reason) {
-+ final Entity entity = this.entity;
-+ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main");
-+ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
-+
-+ EntityLookup.this.removeEntity(entity);
-+
-+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
-+
-+ EntityLookup.this.removeEntityCallback(entity);
-+
-+ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
-+ }
-+ }
-+
-+ protected static final class NoOpCallback implements EntityInLevelCallback {
-+
-+ public static final NoOpCallback INSTANCE = new NoOpCallback();
-+
-+ @Override
-+ public void onMove() {}
-+
-+ @Override
-+ public void onRemove(final Entity.RemovalReason reason) {}
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-@@ -0,0 +1,129 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.entity.LevelCallback;
-+
-+public final class ClientEntityLookup extends EntityLookup {
-+
-+ private final LongOpenHashSet tickingChunks = new LongOpenHashSet();
-+
-+ public ClientEntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
-+ super(world, worldCallback);
-+ }
-+
-+ @Override
-+ protected Boolean blockTicketUpdates() {
-+ // not present on client
-+ return null;
-+ }
-+
-+ @Override
-+ protected void setBlockTicketUpdates(Boolean value) {
-+ // not present on client
-+ }
-+
-+ @Override
-+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
-+ // TODO implement?
-+ }
-+
-+ @Override
-+ protected void checkThread(final Entity entity, final String reason) {
-+ // TODO implement?
-+ }
-+
-+ @Override
-+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+ final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ final ChunkEntitySlices ret = new ChunkEntitySlices(
-+ this.world, chunkX, chunkZ,
-+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null,
-+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ );
-+
-+ // note: not handled by superclass
-+ this.addChunk(chunkX, chunkZ, ret);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
-+ this.removeChunk(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ protected void entitySectionChangeCallback(final Entity entity,
-+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
-+ final int newSectionX, final int newSectionY, final int newSectionZ) {
-+ PlatformHooks.get().entityMove(
-+ entity,
-+ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ),
-+ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ)
-+ );
-+ }
-+
-+ @Override
-+ protected void addEntityCallback(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void removeEntityCallback(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityStartLoaded(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityEndLoaded(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityStartTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityEndTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+ return true;
-+ }
-+
-+ public void markTicking(final long pos) {
-+ if (this.tickingChunks.add(pos)) {
-+ final int chunkX = CoordinateUtils.getChunkX(pos);
-+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
-+ if (this.getChunk(chunkX, chunkZ) != null) {
-+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING);
-+ }
-+ }
-+ }
-+
-+ public void markNonTicking(final long pos) {
-+ if (this.tickingChunks.remove(pos)) {
-+ final int chunkX = CoordinateUtils.getChunkX(pos);
-+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
-+ if (this.getChunk(chunkX, chunkZ) != null) {
-+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL);
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-@@ -0,0 +1,114 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl;
-+
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.entity.LevelCallback;
-+
-+public final class DefaultEntityLookup extends EntityLookup {
-+ public DefaultEntityLookup(final Level world) {
-+ super(world, new DefaultLevelCallback());
-+ }
-+
-+ @Override
-+ protected Boolean blockTicketUpdates() {
-+ return null;
-+ }
-+
-+ @Override
-+ protected void setBlockTicketUpdates(final Boolean value) {}
-+
-+ @Override
-+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {}
-+
-+ @Override
-+ protected void checkThread(final Entity entity, final String reason) {}
-+
-+ @Override
-+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+ final ChunkEntitySlices ret = new ChunkEntitySlices(
-+ this.world, chunkX, chunkZ, FullChunkStatus.FULL,
-+ null, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ );
-+
-+ // note: not handled by superclass
-+ this.addChunk(chunkX, chunkZ, ret);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
-+ this.removeChunk(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ protected void entitySectionChangeCallback(final Entity entity,
-+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
-+ final int newSectionX, final int newSectionY, final int newSectionZ) {
-+
-+ }
-+
-+ @Override
-+ protected void addEntityCallback(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void removeEntityCallback(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityStartLoaded(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityEndLoaded(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityStartTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityEndTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+ return true;
-+ }
-+
-+ protected static final class DefaultLevelCallback implements LevelCallback<Entity> {
-+
-+ @Override
-+ public void onCreated(final Entity entity) {}
-+
-+ @Override
-+ public void onDestroyed(final Entity entity) {}
-+
-+ @Override
-+ public void onTickingStart(final Entity entity) {}
-+
-+ @Override
-+ public void onTickingEnd(final Entity entity) {}
-+
-+ @Override
-+ public void onTrackingStart(final Entity entity) {}
-+
-+ @Override
-+ public void onTrackingEnd(final Entity entity) {}
-+
-+ @Override
-+ public void onSectionChange(final Entity entity) {}
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..58d9187adc188b693b6becc400f766e069bf1bf5
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-@@ -0,0 +1,116 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.common.list.ReferenceList;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+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;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.entity.LevelCallback;
-+
-+public final class ServerEntityLookup extends EntityLookup {
-+
-+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
-+
-+ private final ServerLevel serverWorld;
-+ public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-+
-+ public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
-+ super(world, worldCallback);
-+ this.serverWorld = world;
-+ }
-+
-+ @Override
-+ protected Boolean blockTicketUpdates() {
-+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates();
-+ }
-+
-+ @Override
-+ protected void setBlockTicketUpdates(final Boolean value) {
-+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value);
-+ }
-+
-+ @Override
-+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
-+ TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
-+ }
-+
-+ @Override
-+ protected void checkThread(final Entity entity, final String reason) {
-+ TickThread.ensureTickThread(entity, reason);
-+ }
-+
-+ @Override
-+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+ // loadInEntityChunk will call addChunk for us
-+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager
-+ .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk);
-+ }
-+
-+ @Override
-+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
-+ // entity slices unloading is managed by ticket levels in chunk system
-+ }
-+
-+ @Override
-+ protected void entitySectionChangeCallback(final Entity entity,
-+ final int oldSectionX, final int oldSectionY, final int oldSectionZ,
-+ final int newSectionX, final int newSectionY, final int newSectionZ) {
-+ if (entity instanceof ServerPlayer player) {
-+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player);
-+ }
-+ PlatformHooks.get().entityMove(
-+ entity,
-+ CoordinateUtils.getChunkSectionKey(oldSectionX, oldSectionY, oldSectionZ),
-+ CoordinateUtils.getChunkSectionKey(newSectionX, newSectionY, newSectionZ)
-+ );
-+ }
-+
-+ @Override
-+ protected void addEntityCallback(final Entity entity) {
-+ if (entity instanceof ServerPlayer player) {
-+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player);
-+ }
-+ }
-+
-+ @Override
-+ protected void removeEntityCallback(final Entity entity) {
-+ if (entity instanceof ServerPlayer player) {
-+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player);
-+ }
-+ }
-+
-+ @Override
-+ protected void entityStartLoaded(final Entity entity) {
-+ // Moonrise start - entity tracker
-+ this.trackerEntities.add(entity);
-+ // Moonrise end - entity tracker
-+ }
-+
-+ @Override
-+ protected void entityEndLoaded(final Entity entity) {
-+ // Moonrise start - entity tracker
-+ this.trackerEntities.remove(entity);
-+ // Moonrise end - entity tracker
-+ }
-+
-+ @Override
-+ protected void entityStartTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected void entityEndTicking(final Entity entity) {
-+
-+ }
-+
-+ @Override
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
-@@ -0,0 +1,17 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+
-+public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage {
-+
-+ public ServerLevel moonrise$getWorld();
-+
-+ public void moonrise$onUnload(final long coordinate);
-+
-+ public void moonrise$loadInPoiChunk(final PoiChunk poiChunk);
-+
-+ public void moonrise$checkConsistency(final ChunkAccess chunk);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
-+
-+import net.minecraft.world.entity.ai.village.poi.PoiSection;
-+import java.util.Optional;
-+
-+public interface ChunkSystemPoiSection {
-+
-+ public boolean moonrise$isEmpty();
-+
-+ public Optional<PoiSection> moonrise$asOptional();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-@@ -0,0 +1,204 @@
-+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.DataResult;
-+import net.minecraft.SharedConstants;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.NbtOps;
-+import net.minecraft.nbt.Tag;
-+import net.minecraft.resources.RegistryOps;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.entity.ai.village.poi.PoiManager;
-+import net.minecraft.world.entity.ai.village.poi.PoiSection;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.util.Optional;
-+
-+public final class PoiChunk {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class);
-+
-+ public final ServerLevel world;
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final int minSection;
-+ public final int maxSection;
-+
-+ private final PoiSection[] sections;
-+
-+ private boolean isDirty;
-+ private boolean loaded;
-+
-+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) {
-+ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]);
-+ }
-+
-+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) {
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.minSection = minSection;
-+ this.maxSection = maxSection;
-+ this.sections = sections;
-+ if (this.sections.length != (maxSection - minSection + 1)) {
-+ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length);
-+ }
-+ }
-+
-+ public void load() {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
-+ if (this.loaded) {
-+ return;
-+ }
-+ this.loaded = true;
-+ ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this);
-+ }
-+
-+ public boolean isLoaded() {
-+ return this.loaded;
-+ }
-+
-+ public boolean isEmpty() {
-+ for (final PoiSection section : this.sections) {
-+ if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public PoiSection getOrCreateSection(final int chunkY) {
-+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
-+ final int idx = chunkY - this.minSection;
-+ final PoiSection ret = this.sections[idx];
-+ if (ret != null) {
-+ return ret;
-+ }
-+
-+ final PoiManager poiManager = this.world.getPoiManager();
-+ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ);
-+
-+ return this.sections[idx] = new PoiSection(() -> {
-+ poiManager.setDirty(key);
-+ });
-+ }
-+ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]");
-+ }
-+
-+ public PoiSection getSection(final int chunkY) {
-+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
-+ return this.sections[chunkY - this.minSection];
-+ }
-+ return null;
-+ }
-+
-+ public Optional<PoiSection> getSectionForVanilla(final int chunkY) {
-+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
-+ final PoiSection ret = this.sections[chunkY - this.minSection];
-+ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional();
-+ }
-+ return Optional.empty();
-+ }
-+
-+ public boolean isDirty() {
-+ return this.isDirty;
-+ }
-+
-+ public void setDirty(final boolean dirty) {
-+ this.isDirty = dirty;
-+ }
-+
-+ // returns null if empty
-+ public CompoundTag save() {
-+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess());
-+
-+ final CompoundTag ret = new CompoundTag();
-+ final CompoundTag sections = new CompoundTag();
-+ ret.put("Sections", sections);
-+
-+ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
-+
-+ final ServerLevel world = this.world;
-+ final int chunkX = this.chunkX;
-+ final int chunkZ = this.chunkZ;
-+
-+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
-+ final PoiSection section = this.sections[sectionY - this.minSection];
-+ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
-+ continue;
-+ }
-+
-+ // I do not believe asynchronously converting to CompoundTag is worth the scheduling.
-+ final DataResult<Tag> serializedResult = PoiSection.Packed.CODEC.encodeStart(registryOps, section.pack());
-+ final int finalSectionY = sectionY;
-+ final Tag serialized = serializedResult.resultOrPartial((final String description) -> {
-+ LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
-+ }).orElse(null);
-+ if (serialized == null) {
-+ // failed, should be logged from the resultOrPartial
-+ continue;
-+ }
-+
-+ sections.put(Integer.toString(sectionY), serialized);
-+ }
-+
-+ return sections.isEmpty() ? null : ret;
-+ }
-+
-+ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) {
-+ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world));
-+ ret.loaded = true;
-+ return ret;
-+ }
-+
-+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) {
-+ final PoiChunk ret = empty(world, chunkX, chunkZ);
-+
-+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess());
-+
-+ final CompoundTag sections = data.getCompound("Sections");
-+
-+ if (sections.isEmpty()) {
-+ // nothing to parse
-+ return ret;
-+ }
-+
-+ final PoiManager poiManager = world.getPoiManager();
-+
-+ boolean readAnything = false;
-+
-+ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) {
-+ final String key = Integer.toString(sectionY);
-+ if (!sections.contains(key)) {
-+ continue;
-+ }
-+
-+ final CompoundTag section = sections.getCompound(key);
-+ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section);
-+ final int finalSectionY = sectionY;
-+ final PoiSection.Packed packed = deserializeResult.resultOrPartial((final String description) -> {
-+ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
-+ }).orElse(null);
-+
-+ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
-+ final PoiSection deserialized = packed == null ? null : packed.unpack(() -> {
-+ poiManager.setDirty(coordinateKey);
-+ });
-+
-+ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) {
-+ // completely empty, no point in storing this
-+ continue;
-+ }
-+
-+ readAnything = true;
-+ ret.sections[sectionY - ret.minSection] = deserialized;
-+ }
-+
-+ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load
-+
-+ return ret;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-@@ -0,0 +1,13 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
-+
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+import java.io.IOException;
-+
-+public interface ChunkSystemSectionStorage {
-+
-+ public RegionFileStorage moonrise$getRegionStorage();
-+
-+ public void moonrise$close() throws IOException;
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
-@@ -0,0 +1,15 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.player;
-+
-+public interface ChunkSystemServerPlayer {
-+
-+ public boolean moonrise$isRealPlayer();
-+
-+ public void moonrise$setRealPlayer(final boolean real);
-+
-+ public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader();
-+
-+ public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader);
-+
-+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..dd2509996bfd08e8c3f9f2be042229eac6d7692d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-@@ -0,0 +1,1092 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.player;
-+
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+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.MoonriseConstants;
-+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;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
-+import com.google.gson.JsonObject;
-+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongComparator;
-+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import net.minecraft.network.protocol.Packet;
-+import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
-+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
-+import net.minecraft.server.level.ChunkTrackingView;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.server.network.PlayerChunkSender;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.GameRules;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayDeque;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.Function;
-+
-+public final class RegionizedPlayerChunkLoader {
-+
-+ public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
-+ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
-+
-+ public static final int MIN_VIEW_DISTANCE = 2;
-+ public static final int MAX_VIEW_DISTANCE = 32;
-+
-+ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL;
-+ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
-+ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
-+
-+ public static final class ViewDistanceHolder {
-+
-+ private volatile ViewDistances viewDistances;
-+ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class);
-+
-+ public ViewDistanceHolder() {
-+ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1));
-+ }
-+
-+ public ViewDistances getViewDistances() {
-+ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this);
-+ }
-+
-+ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) {
-+ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update);
-+ }
-+
-+ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) {
-+ int failures = 0;
-+ for (ViewDistances curr = this.getViewDistances();;) {
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) {
-+ return;
-+ }
-+ ++failures;
-+ }
-+ }
-+
-+ public void setTickViewDistance(final int distance) {
-+ this.updateViewDistance((final ViewDistances param) -> {
-+ return param.setTickViewDistance(distance);
-+ });
-+ }
-+
-+ public void setLoadViewDistance(final int distance) {
-+ this.updateViewDistance((final ViewDistances param) -> {
-+ return param.setLoadViewDistance(distance);
-+ });
-+ }
-+
-+ public void setSendViewDistance(final int distance) {
-+ this.updateViewDistance((final ViewDistances param) -> {
-+ return param.setSendViewDistance(distance);
-+ });
-+ }
-+
-+ public JsonObject toJson() {
-+ return this.getViewDistances().toJson();
-+ }
-+ }
-+
-+ public static final record ViewDistances(
-+ int tickViewDistance,
-+ int loadViewDistance,
-+ int sendViewDistance
-+ ) {
-+ public ViewDistances setTickViewDistance(final int distance) {
-+ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
-+ }
-+
-+ public ViewDistances setLoadViewDistance(final int distance) {
-+ // note: load view distance = api view distance + 1
-+ if (distance != -1 && (distance < (2 + 1) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE + 1))) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
-+ }
-+
-+ public ViewDistances setSendViewDistance(final int distance) {
-+ // note: send view distance <= load view distance - 1
-+ if (distance != -1 && (distance < (0) || distance > (MoonriseConstants.MAX_VIEW_DISTANCE))) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
-+ }
-+
-+ public JsonObject toJson() {
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.addProperty("tick-view-distance", this.tickViewDistance);
-+ ret.addProperty("load-view-distance", this.loadViewDistance);
-+ ret.addProperty("send-view-distance", this.sendViewDistance);
-+
-+ return ret;
-+ }
-+ }
-+
-+ public static int getAPITickViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = player.serverLevel();
-+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (data == null) {
-+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance();
-+ }
-+ return data.lastTickDistance;
-+ }
-+
-+ public static int getAPIViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = player.serverLevel();
-+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (data == null) {
-+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
-+ }
-+ // view distance = load distance + 1
-+ return data.lastLoadDistance - 1;
-+ }
-+
-+ public static int getAPISendViewDistance(final ServerPlayer player) {
-+ final ServerLevel level = player.serverLevel();
-+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (data == null) {
-+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance();
-+ }
-+ return data.lastSendDistance;
-+ }
-+
-+ private final ServerLevel world;
-+
-+ public RegionizedPlayerChunkLoader(final ServerLevel world) {
-+ this.world = world;
-+ }
-+
-+ public void addPlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
-+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
-+ return;
-+ }
-+
-+ if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) {
-+ throw new IllegalStateException("Player is already added to player chunk loader");
-+ }
-+
-+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
-+
-+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader);
-+ loader.add();
-+ }
-+
-+ public void updatePlayer(final ServerPlayer player) {
-+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (loader != null) {
-+ loader.update();
-+ // update view distances for nearby players
-+ ((ChunkSystemServerLevel)loader.world).moonrise$getNearbyPlayers().tickPlayer(player);
-+ }
-+ }
-+
-+ public void removePlayer(final ServerPlayer player) {
-+ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
-+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
-+ return;
-+ }
-+
-+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+
-+ if (loader == null) {
-+ return;
-+ }
-+
-+ loader.remove();
-+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null);
-+ }
-+
-+ public void setSendDistance(final int distance) {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance);
-+ }
-+
-+ public void setLoadDistance(final int distance) {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance);
-+ }
-+
-+ public void setTickDistance(final int distance) {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance);
-+ }
-+
-+ // Note: follow the player chunk loader so everything stays consistent...
-+ public int getAPITickDistance() {
-+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
-+ -1, distances.tickViewDistance,
-+ -1, distances.loadViewDistance
-+ );
-+ return tickViewDistance;
-+ }
-+
-+ public int getAPIViewDistance() {
-+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
-+ -1, distances.tickViewDistance,
-+ -1, distances.loadViewDistance
-+ );
-+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
-+
-+ // loadDistance = api view distance + 1
-+ return loadDistance - 1;
-+ }
-+
-+ public int getAPISendViewDistance() {
-+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
-+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
-+ -1, distances.tickViewDistance,
-+ -1, distances.loadViewDistance
-+ );
-+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
-+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
-+ loadDistance, -1, -1, distances.sendViewDistance
-+ );
-+
-+ return sendViewDistance;
-+ }
-+
-+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
-+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
-+ }
-+
-+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (loader == null) {
-+ return false;
-+ }
-+
-+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
-+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (loader == null) {
-+ return false;
-+ }
-+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
-+ return true;
-+ }
-+ }
-+ }
-+
-+ return false;
-+ }
-+
-+ public void tick() {
-+ TickThread.ensureTickThread("Cannot tick player chunk loader async");
-+ long currTime = System.nanoTime();
-+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
-+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-+ if (loader == null || loader.removed || loader.world != this.world) {
-+ // not our problem anymore
-+ continue;
-+ }
-+ loader.update(); // can't invoke plugin logic
-+ loader.updateQueues(currTime);
-+ }
-+ }
-+
-+ public static final class PlayerChunkLoaderData {
-+
-+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
-+ private final long id = ID_GENERATOR.incrementAndGet();
-+ private final Long idBoxed = Long.valueOf(this.id);
-+
-+ private static final long MAX_RATE = 10_000L;
-+
-+ private final ServerPlayer player;
-+ private final ServerLevel world;
-+
-+ private int lastChunkX = Integer.MIN_VALUE;
-+ private int lastChunkZ = Integer.MIN_VALUE;
-+
-+ private int lastSendDistance = Integer.MIN_VALUE;
-+ private int lastLoadDistance = Integer.MIN_VALUE;
-+ private int lastTickDistance = Integer.MIN_VALUE;
-+
-+ private int lastSentChunkCenterX = Integer.MIN_VALUE;
-+ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
-+
-+ private int lastSentChunkRadius = Integer.MIN_VALUE;
-+ private int lastSentSimulationDistance = Integer.MIN_VALUE;
-+
-+ private boolean canGenerateChunks = true;
-+
-+ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
-+ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
-+
-+ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
-+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
-+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
-+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
-+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
-+ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
-+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
-+ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
-+ LOADED_TICKET_LEVEL,
-+ LOADED_TICKET_LEVEL,
-+ GENERATED_TICKET_LEVEL,
-+ GENERATED_TICKET_LEVEL,
-+ TICK_TICKET_LEVEL
-+ };
-+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
-+ {
-+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
-+ }
-+
-+ // rate limiting
-+ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
-+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
-+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
-+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
-+
-+ // queues
-+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
-+ final int c1x = CoordinateUtils.getChunkX(c1);
-+ final int c1z = CoordinateUtils.getChunkZ(c1);
-+
-+ final int c2x = CoordinateUtils.getChunkX(c2);
-+ final int c2z = CoordinateUtils.getChunkZ(c2);
-+
-+ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
-+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
-+
-+ return Integer.compare(
-+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
-+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
-+ );
-+ };
-+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
-+
-+ private volatile boolean removed;
-+
-+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
-+ this.world = world;
-+ this.player = player;
-+ }
-+
-+ private void flushDelayedTicketOps() {
-+ if (this.delayedTicketOps.isEmpty()) {
-+ return;
-+ }
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps);
-+ this.delayedTicketOps.clear();
-+ }
-+
-+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
-+ this.delayedTicketOps.addLast(op);
-+ }
-+
-+ private void sendChunk(final int chunkX, final int chunkZ) {
-+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
-+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player);
-+
-+ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ);
-+
-+ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player);
-+ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk);
-+ return;
-+ }
-+ throw new IllegalStateException();
-+ }
-+
-+ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
-+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
-+ return;
-+ }
-+ this.sendUnloadChunkRaw(chunkX, chunkZ);
-+ }
-+
-+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
-+ PlatformHooks.get().onChunkUnWatch(this.world, new ChunkPos(chunkX, chunkZ), this.player);
-+ // Note: Check PlayerChunkSender#dropChunk for other logic
-+ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies
-+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
-+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player);
-+ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ)));
-+ // Paper start - PlayerChunkUnloadEvent
-+ if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+ new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(new ChunkPos(chunkX, chunkZ).longKey), player.getBukkitEntity()).callEvent();
-+ }
-+ // Paper end - PlayerChunkUnloadEvent
-+ }
-+
-+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we only care about remove
-+ }
-+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ parameter.sendUnloadChunk(chunkX, chunkZ);
-+ }
-+ };
-+ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we only care about remove
-+ }
-+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
-+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
-+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
-+ return;
-+ }
-+
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunk,
-+ PLAYER_TICKET_DELAYED, level, parameter.idBoxed,
-+ PLAYER_TICKET, level, parameter.idBoxed
-+ ));
-+ }
-+ };
-+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
-+ @Override
-+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ // do nothing, we will detect ticking chunks when we try to load them
-+ }
-+
-+ @Override
-+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
-+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
-+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
-+ return;
-+ }
-+
-+ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that
-+ // the level is kept for a short period of time
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunk,
-+ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed,
-+ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
-+ ));
-+ // keep chunk at new generated level
-+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
-+ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
-+ ));
-+ }
-+ };
-+
-+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
-+ final int sendRadius) {
-+ // expect sendRadius to be = 1 + target viewable radius
-+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true);
-+ }
-+
-+ private static int getClientViewDistance(final ServerPlayer player) {
-+ final Integer vd = player.requestedViewDistance();
-+ return vd == null ? -1 : Math.max(0, vd.intValue());
-+ }
-+
-+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance,
-+ final int playerLoadViewDistance, final int worldLoadViewDistance) {
-+ return Math.min(
-+ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance,
-+ playerLoadViewDistance < 0 ? (worldLoadViewDistance - 1) : (playerLoadViewDistance - 1)
-+ );
-+ }
-+
-+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
-+ final int worldLoadViewDistance) {
-+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
-+ }
-+
-+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
-+ final int playerSendViewDistance, final int worldSendViewDistance) {
-+ return Math.min(
-+ loadViewDistance - 1,
-+ playerSendViewDistance < 0 ? (!PlatformHooks.get().configAutoConfigSendDistance() || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
-+ );
-+ }
-+
-+ private Packet<?> updateClientChunkRadius(final int radius) {
-+ this.lastSentChunkRadius = radius;
-+ return new ClientboundSetChunkCacheRadiusPacket(radius);
-+ }
-+
-+ private Packet<?> updateClientSimulationDistance(final int distance) {
-+ this.lastSentSimulationDistance = distance;
-+ return new ClientboundSetSimulationDistancePacket(distance);
-+ }
-+
-+ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
-+ this.lastSentChunkCenterX = chunkX;
-+ this.lastSentChunkCenterZ = chunkZ;
-+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
-+ }
-+
-+ private boolean canPlayerGenerateChunks() {
-+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
-+ }
-+
-+ private double getMaxChunkLoadRate() {
-+ final double configRate = PlatformHooks.get().configPlayerMaxLoadRate();
-+
-+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
-+ }
-+
-+ private double getMaxChunkGenRate() {
-+ final double configRate = PlatformHooks.get().configPlayerMaxGenRate();
-+
-+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
-+ }
-+
-+ private double getMaxChunkSendRate() {
-+ final double configRate = PlatformHooks.get().configPlayerMaxSendRate();
-+
-+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
-+ }
-+
-+ private long getMaxChunkLoads() {
-+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-+ long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentLoads();
-+ if (configLimit == 0L) {
-+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
-+ configLimit = Math.max(5L, radiusChunks / 5L);
-+ } else if (configLimit < 0L) {
-+ configLimit = Integer.MAX_VALUE;
-+ } // else: use the value configured
-+ configLimit = configLimit - this.loadingQueue.size();
-+
-+ return configLimit;
-+ }
-+
-+ private long getMaxChunkGenerates() {
-+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-+ long configLimit = (long)PlatformHooks.get().configPlayerMaxConcurrentGens();
-+ if (configLimit == 0L) {
-+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
-+ configLimit = Math.max(5L, radiusChunks / 5L);
-+ } else if (configLimit < 0L) {
-+ configLimit = Integer.MAX_VALUE;
-+ } // else: use the value configured
-+ configLimit = configLimit - this.generatingQueue.size();
-+
-+ return configLimit;
-+ }
-+
-+ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
-+ final int dx = this.lastChunkX - chunkX;
-+ final int dz = this.lastChunkZ - chunkZ;
-+ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded(
-+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
-+ );
-+ }
-+
-+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
-+ final int dx = this.lastChunkX - chunkX;
-+ final int dz = this.lastChunkZ - chunkZ;
-+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
-+ }
-+
-+ private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) {
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+
-+ final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ);
-+ final byte stage = this.chunkTicketStage.get(neighbour);
-+
-+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ void updateQueues(final long time) {
-+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
-+ if (this.removed) {
-+ throw new IllegalStateException("Ticking removed player chunk loader");
-+ }
-+ // update rate limits
-+ final double loadRate = this.getMaxChunkLoadRate();
-+ final double genRate = this.getMaxChunkGenRate();
-+ final double sendRate = this.getMaxChunkSendRate();
-+
-+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
-+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
-+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
-+
-+ // try to progress chunk loads
-+ while (!this.loadingQueue.isEmpty()) {
-+ final long pendingLoadChunk = this.loadingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
-+ final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ);
-+ if (pending == null) {
-+ // nothing to do here
-+ break;
-+ }
-+ // chunk has loaded, so we can take it out of the queue
-+ this.loadingQueue.dequeueLong();
-+
-+ // try to move to generate queue
-+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
-+ if (prev != CHUNK_TICKET_STAGE_LOADING) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
-+ }
-+
-+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
-+ this.genQueue.enqueue(pendingLoadChunk);
-+ } // else: don't want to generate, so just leave it loaded
-+ }
-+
-+ // try to push more chunk loads
-+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
-+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
-+ if (maxLoadsThisTick > 0) {
-+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
-+ for (int i = 0; i < maxLoadsThisTick; ++i) {
-+ final long chunk = this.loadQueue.dequeueLong();
-+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
-+ if (prev != CHUNK_TICKET_STAGE_NONE) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
-+ }
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addOp(
-+ chunk,
-+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ chunks.add(chunk);
-+ this.loadingQueue.enqueue(chunk);
-+ }
-+
-+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
-+ this.flushDelayedTicketOps();
-+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
-+ // load - only generate ticket levels start anything, but they start generation...
-+ // propagate levels
-+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
-+
-+ if (this.removed) {
-+ // process ticket updates may invoke plugin logic, which may remove this player
-+ return;
-+ }
-+
-+ for (int i = 0; i < maxLoadsThisTick; ++i) {
-+ final long queuedLoadChunk = chunks.getLong(i);
-+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
-+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
-+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, Priority.NORMAL, null
-+ );
-+ if (this.removed) {
-+ return;
-+ }
-+ }
-+ }
-+
-+ // try to progress chunk generations
-+ while (!this.generatingQueue.isEmpty()) {
-+ final long pendingGenChunk = this.generatingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
-+ final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ);
-+ if (pending == null) {
-+ // nothing to do here
-+ break;
-+ }
-+
-+ // chunk has generated, so we can take it out of queue
-+ this.generatingQueue.dequeueLong();
-+
-+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
-+ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
-+ }
-+
-+ // try to move to send queue
-+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
-+ this.sendQueue.enqueue(pendingGenChunk);
-+ }
-+ // try to move to tick queue
-+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
-+ this.tickingQueue.enqueue(pendingGenChunk);
-+ }
-+ }
-+
-+ // try to push more chunk generations
-+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
-+ // preview the allocations, as we may not actually utilise all of them
-+ final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens);
-+ long ratedGensThisTick = 0L;
-+ while (!this.genQueue.isEmpty()) {
-+ final long chunkKey = this.genQueue.firstLong();
-+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
-+ final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
-+ if (chunk.getPersistedStatus() != ChunkStatus.FULL) {
-+ // only rate limit actual generations
-+ if ((ratedGensThisTick + 1L) > maxGensThisTick) {
-+ break;
-+ }
-+ ++ratedGensThisTick;
-+ }
-+
-+ this.genQueue.dequeueLong();
-+
-+ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING);
-+ if (prev != CHUNK_TICKET_STAGE_LOADED) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
-+ }
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addAndRemove(
-+ chunkKey,
-+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
-+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ this.generatingQueue.enqueue(chunkKey);
-+ }
-+ // take the allocations we actually used
-+ this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick);
-+
-+ // try to pull ticking chunks
-+ while (!this.tickingQueue.isEmpty()) {
-+ final long pendingTicking = this.tickingQueue.firstLong();
-+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
-+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
-+
-+ if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ,
-+ ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) {
-+ break;
-+ }
-+
-+ // only gets here if all neighbours were marked as generated or ticking themselves
-+ this.tickingQueue.dequeueLong();
-+ this.pushDelayedTicketOp(
-+ ChunkHolderManager.TicketOperation.addAndRemove(
-+ pendingTicking,
-+ PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
-+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
-+ )
-+ );
-+ // note: there is no queue to add after ticking
-+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
-+ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
-+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
-+ }
-+ }
-+
-+ // try to pull sending chunks
-+ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends
-+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
-+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
-+ for (int i = 0; i < maxSendsThisTick; ++i) {
-+ final long pendingSend = this.sendQueue.firstLong();
-+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
-+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
-+ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ);
-+ if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
-+ // nothing to do
-+ // the target chunk may not be owned by this region, but this should be resolved in the future
-+ break;
-+ }
-+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
-+ // not yet post-processed, need to do this so that tile entities can properly be sent to clients
-+ chunk.postProcessGeneration(this.world);
-+ // check if there was any recursive action
-+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
-+ return;
-+ } // else: good to dequeue and send, fall through
-+ }
-+ this.sendQueue.dequeueLong();
-+
-+ this.sendChunk(pendingSendX, pendingSendZ);
-+
-+ if (this.removed) {
-+ // sendChunk may invoke plugin logic
-+ return;
-+ }
-+ }
-+
-+ this.flushDelayedTicketOps();
-+ }
-+
-+ void add() {
-+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Adding removed player chunk loader");
-+ }
-+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
-+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
-+ final int chunkX = this.player.chunkPosition().x;
-+ final int chunkZ = this.player.chunkPosition().z;
-+
-+ final int tickViewDistance = getTickDistance(
-+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
-+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
-+ );
-+ // load view cannot be less-than tick view + 1
-+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
-+ // send view cannot be greater-than load view
-+ final int clientViewDistance = getClientViewDistance(this.player);
-+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
-+
-+ // send view distances
-+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
-+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
-+
-+ // add to distance maps
-+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1);
-+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
-+ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
-+
-+ // update chunk center
-+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
-+
-+ // reset limiters, they will start at a zero allocation
-+ final long time = System.nanoTime();
-+ this.chunkLoadTicketLimiter.reset(time);
-+ this.chunkGenerateTicketLimiter.reset(time);
-+ this.chunkSendLimiter.reset(time);
-+
-+ // now we can update
-+ this.update();
-+ }
-+
-+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
-+ return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ));
-+ }
-+
-+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
-+ final BelowZeroRetrogen belowZeroRetrogen;
-+ // see PortalForcer#findPortalAround
-+ return chunkAccess != null && (
-+ chunkAccess.getPersistedStatus() == ChunkStatus.FULL ||
-+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))
-+ );
-+ }
-+
-+ void update() {
-+ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Updating removed player chunk loader");
-+ }
-+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
-+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
-+
-+ final int tickViewDistance = getTickDistance(
-+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
-+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
-+ );
-+ // load view cannot be less-than tick view + 1
-+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
-+ // send view cannot be greater-than load view
-+ final int clientViewDistance = getClientViewDistance(this.player);
-+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
-+
-+ final ChunkPos playerPos = this.player.chunkPosition();
-+ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
-+ final int currentChunkX = playerPos.x;
-+ final int currentChunkZ = playerPos.z;
-+
-+ final int prevChunkX = this.lastChunkX;
-+ final int prevChunkZ = this.lastChunkZ;
-+
-+ if (
-+ // has view distance stayed the same?
-+ sendViewDistance == this.lastSendDistance
-+ && loadViewDistance == this.lastLoadDistance
-+ && tickViewDistance == this.lastTickDistance
-+
-+ // has our chunk stayed the same?
-+ && prevChunkX == currentChunkX
-+ && prevChunkZ == currentChunkZ
-+
-+ // can we still generate chunks?
-+ && this.canGenerateChunks == canGenerateChunks
-+ ) {
-+ // nothing we care about changed, so we're not re-calculating
-+ return;
-+ }
-+
-+ // update distance maps
-+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1);
-+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
-+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
-+ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) {
-+ throw new IllegalStateException();
-+ }
-+
-+ // update VDs for client
-+ // this should be after the distance map updates, as they will send unload packets
-+ if (this.lastSentChunkRadius != sendViewDistance) {
-+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
-+ }
-+ if (this.lastSentSimulationDistance != tickViewDistance) {
-+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
-+ }
-+
-+ this.sendQueue.clear();
-+ this.tickingQueue.clear();
-+ this.generatingQueue.clear();
-+ this.genQueue.clear();
-+ this.loadingQueue.clear();
-+ this.loadQueue.clear();
-+
-+ this.lastChunkX = currentChunkX;
-+ this.lastChunkZ = currentChunkZ;
-+ this.lastSendDistance = sendViewDistance;
-+ this.lastLoadDistance = loadViewDistance;
-+ this.lastTickDistance = tickViewDistance;
-+ this.canGenerateChunks = canGenerateChunks;
-+
-+ // +1 since we need to load chunks +1 around the load view distance...
-+ final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1);
-+ // the iteration order is by increasing manhattan distance - so, we do NOT need to
-+ // sort anything in the queue!
-+ for (final long deltaChunk : toIterate) {
-+ final int dx = CoordinateUtils.getChunkX(deltaChunk);
-+ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
-+ final int chunkX = dx + currentChunkX;
-+ final int chunkZ = dz + currentChunkZ;
-+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
-+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
-+
-+ // since chunk sending is not by radius alone, we need an extra check here to account for
-+ // everything <= sendDistance
-+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
-+ // the dist <= view check
-+ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1))
-+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
-+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
-+
-+ if (!sendChunk && sentChunk) {
-+ // have sent the chunk, but don't want it anymore
-+ // unload it now
-+ this.sendUnloadChunkRaw(chunkX, chunkZ);
-+ }
-+
-+ final byte stage = this.chunkTicketStage.get(chunk);
-+ switch (stage) {
-+ case CHUNK_TICKET_STAGE_NONE: {
-+ // we want the chunk to be at least loaded
-+ this.loadQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_LOADING: {
-+ this.loadingQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_LOADED: {
-+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
-+ this.genQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_GENERATING: {
-+ this.generatingQueue.enqueue(chunk);
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_GENERATED: {
-+ if (sendChunk && !sentChunk) {
-+ this.sendQueue.enqueue(chunk);
-+ }
-+ if (squareDistance <= tickViewDistance) {
-+ this.tickingQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ case CHUNK_TICKET_STAGE_TICK: {
-+ if (sendChunk && !sentChunk) {
-+ this.sendQueue.enqueue(chunk);
-+ }
-+ break;
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown stage: " + stage);
-+ }
-+ }
-+ }
-+
-+ // update the chunk center
-+ // this must be done last so that the client does not ignore any of our unload chunk packets above
-+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
-+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
-+ }
-+
-+ this.flushDelayedTicketOps();
-+ }
-+
-+ void remove() {
-+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
-+ if (this.removed) {
-+ throw new IllegalStateException("Removing removed player chunk loader");
-+ }
-+ this.removed = true;
-+ // sends the chunk unload packets
-+ this.broadcastMap.remove();
-+ // cleans up loading/generating tickets
-+ this.loadTicketCleanup.remove();
-+ // cleans up ticking tickets
-+ this.tickMap.remove();
-+
-+ // purge queues
-+ this.sendQueue.clear();
-+ this.tickingQueue.clear();
-+ this.generatingQueue.clear();
-+ this.genQueue.clear();
-+ this.loadingQueue.clear();
-+ this.loadQueue.clear();
-+
-+ // flush ticket changes
-+ this.flushDelayedTicketOps();
-+
-+ // now all tickets should be removed, which is all of our external state
-+ }
-+
-+ public LongOpenHashSet getSentChunksRaw() {
-+ return this.sentChunks;
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-@@ -0,0 +1,144 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.queue;
-+
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonElement;
-+import com.google.gson.JsonObject;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+import java.util.ArrayList;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.concurrent.atomic.AtomicLong;
-+
-+public final class ChunkUnloadQueue {
-+
-+ public final int coordinateShift;
-+ private final AtomicLong orderGenerator = new AtomicLong();
-+ private final ConcurrentLong2ReferenceChainedHashTable<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>();
-+
-+ /*
-+ * Note: write operations do not occur in parallel for any given section.
-+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly
-+ */
-+
-+ public ChunkUnloadQueue(final int coordinateShift) {
-+ this.coordinateShift = coordinateShift;
-+ }
-+
-+ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
-+
-+ public List<SectionToUnload> retrieveForAllRegions() {
-+ final List<SectionToUnload> ret = new ArrayList<>();
-+
-+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
-+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next();
-+ final long key = entry.getKey();
-+ final UnloadSection section = entry.getValue();
-+ final int sectionX = CoordinateUtils.getChunkX(key);
-+ final int sectionZ = CoordinateUtils.getChunkZ(key);
-+
-+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
-+ }
-+
-+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
-+ return Long.compare(s1.order, s2.order);
-+ });
-+
-+ return ret;
-+ }
-+
-+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
-+ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ));
-+ }
-+
-+ public UnloadSection removeSection(final int sectionX, final int sectionZ) {
-+ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ));
-+ }
-+
-+ // write operation
-+ public boolean addChunk(final int chunkX, final int chunkZ) {
-+ // write operations do not occur in parallel for a given section
-+ final int shift = this.coordinateShift;
-+ final int sectionX = chunkX >> shift;
-+ final int sectionZ = chunkZ >> shift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ UnloadSection section = this.unloadSections.get(sectionKey);
-+ if (section == null) {
-+ section = new UnloadSection(this.orderGenerator.getAndIncrement());
-+ this.unloadSections.put(sectionKey, section);
-+ }
-+
-+ return section.chunks.add(chunkKey);
-+ }
-+
-+ // write operation
-+ public boolean removeChunk(final int chunkX, final int chunkZ) {
-+ // write operations do not occur in parallel for a given section
-+ final int shift = this.coordinateShift;
-+ final int sectionX = chunkX >> shift;
-+ final int sectionZ = chunkZ >> shift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final UnloadSection section = this.unloadSections.get(sectionKey);
-+
-+ if (section == null) {
-+ return false;
-+ }
-+
-+ if (!section.chunks.remove(chunkKey)) {
-+ return false;
-+ }
-+
-+ if (section.chunks.isEmpty()) {
-+ this.unloadSections.remove(sectionKey);
-+ }
-+
-+ return true;
-+ }
-+
-+ public JsonElement toDebugJson() {
-+ final JsonArray ret = new JsonArray();
-+
-+ for (final SectionToUnload section : this.retrieveForAllRegions()) {
-+ final JsonObject sectionJson = new JsonObject();
-+ ret.add(sectionJson);
-+
-+ sectionJson.addProperty("sectionX", section.sectionX());
-+ sectionJson.addProperty("sectionZ", section.sectionX());
-+ sectionJson.addProperty("order", section.order());
-+
-+ final JsonArray coordinates = new JsonArray();
-+ sectionJson.add("coordinates", coordinates);
-+
-+ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ());
-+ if (actualSection != null) {
-+ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) {
-+ final long coordinate = iterator.nextLong();
-+
-+ final JsonObject coordinateJson = new JsonObject();
-+ coordinates.add(coordinateJson);
-+
-+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate)));
-+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate)));
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static final class UnloadSection {
-+
-+ public final long order;
-+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet();
-+
-+ public UnloadSection(final long order) {
-+ this.order = order;
-+ }
-+ }
-+}
-\ No newline at end of file
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3990834a41116682d6ae779a3bf24b0fd989d97d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-@@ -0,0 +1,1457 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+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.common.util.ChunkSystem;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket;
-+import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonObject;
-+import com.mojang.logging.LogUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ByteMap;
-+import it.unimi.dsi.fastutil.longs.Long2IntMap;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkLevel;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.Ticket;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.util.SortedArraySet;
-+import net.minecraft.util.Unit;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import org.slf4j.Logger;
-+import java.io.IOException;
-+import java.text.DecimalFormat;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.Collection;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Objects;
-+import java.util.PrimitiveIterator;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicReference;
-+import java.util.concurrent.locks.LockSupport;
-+import java.util.function.Predicate;
-+
-+public final class ChunkHolderManager {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
-+ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
-+ public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL;
-+ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive
-+
-+ public static final TicketType<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
-+
-+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
-+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
-+ public final ReentrantAreaLock ticketLockArea;
-+
-+ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>();
-+ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>();
-+ final ChunkUnloadQueue unloadQueue;
-+
-+ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
-+ private final ServerLevel world;
-+ private final ChunkTaskScheduler taskScheduler;
-+ private long currentTick;
-+
-+ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
-+ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
-+ if (c1 == c2) {
-+ return 0;
-+ }
-+
-+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
-+
-+ if (saveTickCompare != 0) {
-+ return saveTickCompare;
-+ }
-+
-+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
-+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
-+
-+ if (coord1 == coord2) {
-+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
-+ }
-+
-+ return Long.compare(coord1, coord2);
-+ });
-+
-+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
-+ this.world = world;
-+ this.taskScheduler = taskScheduler;
-+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift());
-+ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
-+ }
-+
-+ public boolean processTicketUpdates(final int posX, final int posZ) {
-+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
-+ final int ticketMask = (1 << ticketShift) - 1;
-+ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
-+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
-+ final boolean ret;
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
-+ ((posX >> ticketShift) - 1) << ticketShift,
-+ ((posZ >> ticketShift) - 1) << ticketShift,
-+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
-+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
-+ );
-+ try {
-+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ this.addChangedStatuses(changedFullStatus);
-+
-+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
-+ scheduledTasks.get(i).schedule();
-+ }
-+
-+ return ret;
-+ }
-+
-+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks,
-+ final List<NewChunkHolder> changedFullStatus) {
-+ return this.ticketLevelPropagator.performUpdate(
-+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
-+ );
-+ }
-+
-+ public List<ChunkHolder> getOldChunkHolders() {
-+ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
-+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
-+ ret.add(iterator.next().vanillaChunkHolder);
-+ }
-+ return ret;
-+ }
-+
-+ public List<NewChunkHolder> getChunkHolders() {
-+ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
-+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
-+ ret.add(iterator.next());
-+ }
-+ return ret;
-+ }
-+
-+ public int size() {
-+ return this.chunkHolders.size();
-+ }
-+
-+ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks
-+ public Iterable<ChunkHolder> getOldChunkHoldersIterable() {
-+ return new Iterable<ChunkHolder>() {
-+ @Override
-+ public Iterator<ChunkHolder> iterator() {
-+ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator();
-+ return new Iterator<ChunkHolder>() {
-+ @Override
-+ public boolean hasNext() {
-+ return iterator.hasNext();
-+ }
-+
-+ @Override
-+ public ChunkHolder next() {
-+ return iterator.next().vanillaChunkHolder;
-+ }
-+ };
-+ }
-+ };
-+ }
-+
-+ public void close(final boolean save, final boolean halt) {
-+ TickThread.ensureTickThread("Closing world off-main");
-+ if (halt) {
-+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
-+ LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ } else {
-+ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ }
-+ }
-+
-+ if (save) {
-+ this.saveAllChunks(true, true, true);
-+ }
-+
-+ MoonriseRegionFileIO.flush(this.world);
-+
-+ if (halt) {
-+ LOGGER.info("Waiting 60s for chunk I/O to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ if (!this.taskScheduler.haltIO(true, TimeUnit.SECONDS.toNanos(60L))) {
-+ LOGGER.warn("Failed to halt I/O tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ } else {
-+ LOGGER.info("Halted I/O scheduler for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ }
-+ }
-+
-+ // kill regionfile cache
-+ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) {
-+ try {
-+ MoonriseRegionFileIO.getControllerFor(this.world, type).getCache().close();
-+ } catch (final IOException ex) {
-+ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex);
-+ }
-+ }
-+
-+ this.taskScheduler.setShutdown(true);
-+ }
-+
-+ void ensureInAutosave(final NewChunkHolder holder) {
-+ if (!this.autoSaveQueue.contains(holder)) {
-+ holder.lastAutoSave = this.currentTick;
-+ this.autoSaveQueue.add(holder);
-+ }
-+ }
-+
-+ public void autoSave() {
-+ final List<NewChunkHolder> reschedule = new ArrayList<>();
-+ final long currentTick = this.currentTick;
-+ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world));
-+ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world);
-+ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
-+ final NewChunkHolder holder = this.autoSaveQueue.first();
-+
-+ if (holder.lastAutoSave > maxSaveTime) {
-+ break;
-+ }
-+
-+ this.autoSaveQueue.remove(holder);
-+
-+ holder.lastAutoSave = currentTick;
-+ if (holder.save(false) != null) {
-+ ++autoSaved;
-+ }
-+
-+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
-+ reschedule.add(holder);
-+ }
-+ }
-+
-+ for (final NewChunkHolder holder : reschedule) {
-+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
-+ this.autoSaveQueue.add(holder);
-+ }
-+ }
-+ }
-+
-+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
-+ final List<NewChunkHolder> holders = this.getChunkHolders();
-+
-+ if (logProgress) {
-+ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ }
-+
-+ final DecimalFormat format = new DecimalFormat("#0.00");
-+
-+ int saved = 0;
-+
-+ long start = System.nanoTime();
-+ long lastLog = start;
-+ final int flushInterval = 200;
-+ int lastFlush = 0;
-+
-+ int savedChunk = 0;
-+ int savedEntity = 0;
-+ int savedPoi = 0;
-+
-+ if (shutdown) {
-+ // Normal unload process does not occur during shutdown: fire event manually
-+ // for mods that expect ChunkEvent.Unload to fire on shutdown (before LevelEvent.Unload)
-+ for (int i = 0, len = holders.size(); i < len; ++i) {
-+ final NewChunkHolder holder = holders.get(i);
-+ if (holder.getCurrentChunk() instanceof LevelChunk levelChunk) {
-+ PlatformHooks.get().chunkUnloadFromWorld(levelChunk);
-+ }
-+ }
-+ }
-+ for (int i = 0, len = holders.size(); i < len; ++i) {
-+ final NewChunkHolder holder = holders.get(i);
-+ try {
-+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
-+ if (saveStat != null) {
-+ if (saveStat.savedChunk()) {
-+ ++savedChunk;
-+ ++saved;
-+ }
-+ if (saveStat.savedEntityChunk()) {
-+ ++savedEntity;
-+ ++saved;
-+ }
-+ if (saveStat.savedPoiChunk()) {
-+ ++savedPoi;
-+ ++saved;
-+ }
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
-+ }
-+ if (flush && (saved - lastFlush) > (flushInterval / 2)) {
-+ lastFlush = saved;
-+ MoonriseRegionFileIO.partialFlush(this.world, flushInterval / 2);
-+ }
-+ if (logProgress) {
-+ final long currTime = System.nanoTime();
-+ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) {
-+ lastLog = currTime;
-+ LOGGER.info(
-+ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi
-+ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "', progress: "
-+ + format.format((double)(i+1)/(double)len * 100.0)
-+ );
-+ }
-+ }
-+ }
-+ if (flush) {
-+ MoonriseRegionFileIO.flush(this.world);
-+ try {
-+ MoonriseRegionFileIO.flushRegionStorages(this.world);
-+ } catch (final IOException ex) {
-+ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex);
-+ }
-+ }
-+ if (logProgress) {
-+ LOGGER.info(
-+ "Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi
-+ + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in "
-+ + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"
-+ );
-+ }
-+ }
-+
-+ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() {
-+ @Override
-+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) {
-+ // first the necessary chunkholders must be created, so just update the ticket levels
-+ for (final Iterator<Long2ByteMap.Entry> iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Long2ByteMap.Entry entry = iterator.next();
-+ final long key = entry.getLongKey();
-+ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue());
-+
-+ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
-+ if (current == null && newLevel > MAX_TICKET_LEVEL) {
-+ // not loaded and it shouldn't be loaded!
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel();
-+ if (currentLevel == newLevel) {
-+ // nothing to do
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ if (current == null) {
-+ // must create
-+ current = ChunkHolderManager.this.createChunkHolder(key);
-+ ChunkHolderManager.this.chunkHolders.put(key, current);
-+ current.updateTicketLevel(newLevel);
-+ } else {
-+ current.updateTicketLevel(newLevel);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
-+ final List<NewChunkHolder> changedFullStatus) {
-+ final List<ChunkProgressionTask> prev = CURRENT_TICKET_UPDATE_SCHEDULING.get();
-+ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks);
-+ try {
-+ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) {
-+ final long key = iterator.nextLong();
-+ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
-+
-+ if (current == null) {
-+ throw new IllegalStateException("Expected chunk holder to be created");
-+ }
-+
-+ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus);
-+ }
-+ } finally {
-+ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev);
-+ }
-+ }
-+ };
-+ // function for converting between ticket levels and propagator levels and vice versa
-+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects
-+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator
-+ // and the levels we get out of the propagator
-+
-+ public static int convertBetweenTicketLevels(final int level) {
-+ return ChunkLevel.MAX_LEVEL - level + 1;
-+ }
-+
-+ public String getTicketDebugString(final long coordinate) {
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
-+ try {
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
-+
-+ return tickets != null ? tickets.first().toString() : "no_ticket";
-+ } finally {
-+ if (ticketLock != null) {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+ }
-+
-+ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() {
-+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
-+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
-+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
-+ final long coord = iterator.nextLong();
-+ sections.computeIfAbsent(
-+ CoordinateUtils.getChunkKey(
-+ CoordinateUtils.getChunkX(coord) >> sectionShift,
-+ CoordinateUtils.getChunkZ(coord) >> sectionShift
-+ ),
-+ (final long keyInMap) -> {
-+ return new LongArrayList();
-+ }
-+ ).add(coord);
-+ }
-+
-+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
-+ iterator.hasNext();) {
-+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
-+ final long sectionKey = entry.getLongKey();
-+ final LongArrayList coordinates = entry.getValue();
-+
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
-+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
-+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
-+ );
-+ try {
-+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
-+ final long coord = iterator2.nextLong();
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord);
-+ if (tickets == null) {
-+ // removed before we acquired lock
-+ continue;
-+ }
-+ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy());
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ // Paper start
-+ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) {
-+ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret;
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z);
-+ try {
-+ final long coordinate = CoordinateUtils.getChunkKey(x, z);
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
-+
-+ if (tickets == null) {
-+ return java.util.Collections.emptyList();
-+ }
-+
-+ ret = com.google.common.collect.ImmutableList.builder();
-+ for (Ticket<?> ticket : tickets) {
-+ if (ticket.getType() == TicketType.PLUGIN_TICKET) {
-+ ret.add((org.bukkit.plugin.Plugin)ticket.key);
-+ }
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ return ret.build();
-+ }
-+ // Paper end
-+
-+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
-+ if (ticketLevel > ChunkLevel.MAX_LEVEL) {
-+ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
-+ } else {
-+ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel));
-+ }
-+ }
-+
-+ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
-+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1;
-+ }
-+
-+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level,
-+ final T identifier) {
-+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
-+ }
-+
-+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level,
-+ final T identifier) {
-+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
-+ }
-+
-+ private void addExpireCount(final int chunkX, final int chunkZ) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
-+ final long sectionKey = CoordinateUtils.getChunkKey(
-+ chunkX >> sectionShift,
-+ chunkZ >> sectionShift
-+ );
-+
-+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> {
-+ return new Long2IntOpenHashMap();
-+ }).addTo(chunkKey, 1);
-+ }
-+
-+ private void removeExpireCount(final int chunkX, final int chunkZ) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
-+ final long sectionKey = CoordinateUtils.getChunkKey(
-+ chunkX >> sectionShift,
-+ chunkZ >> sectionShift
-+ );
-+
-+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey);
-+ final int prevCount = removeCounts.addTo(chunkKey, -1);
-+
-+ if (prevCount == 1) {
-+ removeCounts.remove(chunkKey);
-+ if (removeCounts.isEmpty()) {
-+ this.sectionToChunkToExpireCount.remove(sectionKey);
-+ }
-+ }
-+ }
-+
-+ // supposed to return true if the ticket was added and did not replace another
-+ // but, we always return false if the ticket cannot be added
-+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
-+ return this.addTicketAtLevel(type, chunk, level, identifier, true);
-+ }
-+
-+ <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
-+ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout;
-+ if (level > MAX_TICKET_LEVEL) {
-+ return false;
-+ }
-+
-+ final int chunkX = CoordinateUtils.getChunkX(chunk);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
-+ final Ticket<T> ticket = new Ticket<>(type, level, identifier);
-+ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
-+
-+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
-+ try {
-+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
-+ return SortedArraySet.create(4);
-+ });
-+
-+ final int levelBefore = getTicketLevelAt(ticketsAtChunk);
-+ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket);
-+ final int levelAfter = getTicketLevelAt(ticketsAtChunk);
-+
-+ if (current != ticket) {
-+ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay();
-+ if (removeDelay != oldRemoveDelay) {
-+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) {
-+ this.removeExpireCount(chunkX, chunkZ);
-+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) {
-+ // since old != new, we have that NO_TIMEOUT_MARKER != new
-+ this.addExpireCount(chunkX, chunkZ);
-+ }
-+ }
-+ } else {
-+ if (removeDelay != NO_TIMEOUT_MARKER) {
-+ this.addExpireCount(chunkX, chunkZ);
-+ }
-+ }
-+
-+ if (levelBefore != levelAfter) {
-+ this.updateTicketLevel(chunk, levelAfter);
-+ }
-+
-+ return current == ticket;
-+ } finally {
-+ if (ticketLock != null) {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+ }
-+
-+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, final T identifier) {
-+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
-+ }
-+
-+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, final T identifier) {
-+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
-+ }
-+
-+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
-+ return this.removeTicketAtLevel(type, chunk, level, identifier, true);
-+ }
-+
-+ <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
-+ if (level > MAX_TICKET_LEVEL) {
-+ return false;
-+ }
-+
-+ final int chunkX = CoordinateUtils.getChunkX(chunk);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
-+ final Ticket<T> probe = new Ticket<>(type, level, identifier);
-+
-+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
-+ try {
-+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk);
-+ if (ticketsAtChunk == null) {
-+ return false;
-+ }
-+
-+ final int oldLevel = getTicketLevelAt(ticketsAtChunk);
-+ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe);
-+
-+ if (ticket == null) {
-+ return false;
-+ }
-+
-+ final int newLevel = getTicketLevelAt(ticketsAtChunk);
-+ // we should not change the ticket levels while the target region may be ticking
-+ if (oldLevel != newLevel) {
-+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk));
-+ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout));
-+ if (ticketsAtChunk.add(unknownTicket)) {
-+ this.addExpireCount(chunkX, chunkZ);
-+ } else {
-+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
-+ }
-+ }
-+
-+ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay();
-+ if (removeDelay != NO_TIMEOUT_MARKER) {
-+ this.removeExpireCount(chunkX, chunkZ);
-+ }
-+
-+ return true;
-+ } finally {
-+ if (ticketLock != null) {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+ }
-+
-+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
-+ public <T, V> void addAndRemoveTickets(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
-+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
-+ try {
-+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
-+ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false);
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+
-+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
-+ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
-+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
-+ try {
-+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) {
-+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
-+ return true;
-+ }
-+ return false;
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+
-+ public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) {
-+ if (ticketLevel > MAX_TICKET_LEVEL) {
-+ return;
-+ }
-+
-+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
-+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
-+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
-+ final long coord = iterator.nextLong();
-+ sections.computeIfAbsent(
-+ CoordinateUtils.getChunkKey(
-+ CoordinateUtils.getChunkX(coord) >> sectionShift,
-+ CoordinateUtils.getChunkZ(coord) >> sectionShift
-+ ),
-+ (final long keyInMap) -> {
-+ return new LongArrayList();
-+ }
-+ ).add(coord);
-+ }
-+
-+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
-+ iterator.hasNext();) {
-+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
-+ final long sectionKey = entry.getLongKey();
-+ final LongArrayList coordinates = entry.getValue();
-+
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
-+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
-+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
-+ );
-+ try {
-+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
-+ final long coord = iterator2.nextLong();
-+ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+ }
-+
-+ public void tick() {
-+ ++this.currentTick;
-+
-+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
-+
-+ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> {
-+ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay();
-+ if (removeDelay == NO_TIMEOUT_MARKER) {
-+ return false;
-+ }
-+ --removeDelay;
-+ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
-+ return removeDelay <= 0L;
-+ };
-+
-+ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
-+ final long sectionKey = iterator.nextLong();
-+
-+ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
-+ // removed concurrently
-+ continue;
-+ }
-+
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
-+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
-+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
-+ );
-+
-+ try {
-+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey);
-+ if (chunkToExpireCount == null) {
-+ // lost to some race
-+ continue;
-+ }
-+
-+ for (final Iterator<Long2IntMap.Entry> iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) {
-+ final Long2IntMap.Entry entry = iterator1.next();
-+
-+ final long chunkKey = entry.getLongKey();
-+ final int expireCount = entry.getIntValue();
-+
-+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey);
-+ final int levelBefore = getTicketLevelAt(tickets);
-+
-+ final int sizeBefore = tickets.size();
-+ tickets.removeIf(expireNow);
-+ final int sizeAfter = tickets.size();
-+ final int levelAfter = getTicketLevelAt(tickets);
-+
-+ if (tickets.isEmpty()) {
-+ this.tickets.remove(chunkKey);
-+ }
-+ if (levelBefore != levelAfter) {
-+ this.updateTicketLevel(chunkKey, levelAfter);
-+ }
-+
-+ final int newExpireCount = expireCount - (sizeBefore - sizeAfter);
-+
-+ if (newExpireCount == expireCount) {
-+ continue;
-+ }
-+
-+ if (newExpireCount != 0) {
-+ entry.setValue(newExpireCount);
-+ } else {
-+ iterator1.remove();
-+ }
-+ }
-+
-+ if (chunkToExpireCount.isEmpty()) {
-+ this.sectionToChunkToExpireCount.remove(sectionKey);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+ }
-+
-+ this.processTicketUpdates();
-+ }
-+
-+ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) {
-+ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public NewChunkHolder getChunkHolder(final long position) {
-+ return this.chunkHolders.get(position);
-+ }
-+
-+ public void raisePriority(final int x, final int z, final Priority priority) {
-+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
-+ if (chunkHolder != null) {
-+ chunkHolder.raisePriority(priority);
-+ }
-+ }
-+
-+ public void setPriority(final int x, final int z, final Priority priority) {
-+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
-+ if (chunkHolder != null) {
-+ chunkHolder.setPriority(priority);
-+ }
-+ }
-+
-+ public void lowerPriority(final int x, final int z, final Priority priority) {
-+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
-+ if (chunkHolder != null) {
-+ chunkHolder.lowerPriority(priority);
-+ }
-+ }
-+
-+ private NewChunkHolder createChunkHolder(final long position) {
-+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
-+
-+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
-+
-+ return ret;
-+ }
-+
-+ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure
-+ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders,
-+ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate
-+ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) {
-+ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ private NewChunkHolder getOrCreateChunkHolder(final long position) {
-+ final int chunkX = CoordinateUtils.getChunkX(position);
-+ final int chunkZ = CoordinateUtils.getChunkZ(position);
-+
-+ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
-+ throw new IllegalStateException("Must hold ticket level update lock!");
-+ }
-+ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
-+ throw new IllegalStateException("Must hold scheduler lock!!");
-+ }
-+
-+ // we could just acquire these locks, but...
-+ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns
-+
-+ NewChunkHolder current = this.chunkHolders.get(position);
-+ if (current != null) {
-+ return current;
-+ }
-+
-+ current = this.createChunkHolder(position);
-+ this.chunkHolders.put(position, current);
-+
-+
-+ return current;
-+ }
-+
-+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
-+ ChunkEntitySlices ret;
-+
-+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
-+ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
-+ return ret;
-+ }
-+
-+ final AtomicBoolean isCompleted = new AtomicBoolean();
-+ final Thread waiter = Thread.currentThread();
-+ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId();
-+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
-+ try {
-+ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
-+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
-+ try {
-+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
-+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
-+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
-+ return ret;
-+ }
-+
-+ if (!transientChunk) {
-+ if (current.isEntityChunkNBTLoaded()) {
-+ isCompleted.setPlain(true);
-+ } else {
-+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> {
-+ isCompleted.set(true);
-+ LockSupport.unpark(waiter);
-+ });
-+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
-+
-+ if (entityLoad != null) {
-+ entityLoad.raisePriority(Priority.BLOCKING);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ if (loadTask != null) {
-+ loadTask.schedule();
-+ }
-+
-+ if (!transientChunk) {
-+ // Note: no need to busy wait on the chunk queue, entity load will complete off-main
-+ boolean interrupted = false;
-+ while (!isCompleted.get()) {
-+ interrupted |= Thread.interrupted();
-+ LockSupport.park();
-+ }
-+
-+ if (interrupted) {
-+ Thread.currentThread().interrupt();
-+ }
-+ }
-+
-+ // now that the entity data is loaded, we can load it into the world
-+
-+ ret = current.loadInEntityChunk(transientChunk);
-+
-+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
-+
-+ return ret;
-+ }
-+
-+ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) {
-+ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ);
-+ if (holder != null) {
-+ final PoiChunk ret = holder.getPoiChunk();
-+ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret;
-+ }
-+ return null;
-+ }
-+
-+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
-+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
-+ PoiChunk ret;
-+
-+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
-+ if (current != null && (ret = current.getPoiChunk()) != null) {
-+ ret.load();
-+ return ret;
-+ }
-+
-+ final AtomicReference<PoiChunk> completed = new AtomicReference<>();
-+ final AtomicBoolean isCompleted = new AtomicBoolean();
-+ final Thread waiter = Thread.currentThread();
-+ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId();
-+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
-+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
-+ try {
-+ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
-+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
-+ try {
-+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
-+ if (null == (ret = current.getPoiChunk())) {
-+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> {
-+ completed.setPlain(result.left());
-+ isCompleted.set(true);
-+ LockSupport.unpark(waiter);
-+ });
-+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.raisePriority(Priority.BLOCKING);
-+ }
-+ }
-+ } finally {
-+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ if (loadTask != null) {
-+ loadTask.schedule();
-+
-+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main
-+
-+ boolean interrupted = false;
-+ while (!isCompleted.get()) {
-+ interrupted |= Thread.interrupted();
-+ LockSupport.park();
-+ }
-+
-+ if (interrupted) {
-+ Thread.currentThread().interrupt();
-+ }
-+
-+ ret = completed.getPlain();
-+ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked
-+
-+ ret.load();
-+
-+ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
-+
-+ return ret;
-+ }
-+
-+ void addChangedStatuses(final List<NewChunkHolder> changedFullStatus) {
-+ if (changedFullStatus.isEmpty()) {
-+ return;
-+ }
-+ if (!TickThread.isTickThread()) {
-+ this.taskScheduler.scheduleChunkTask(() -> {
-+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
-+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
-+ }
-+
-+ ChunkHolderManager.this.processPendingFullUpdate();
-+ }, Priority.HIGHEST);
-+ } else {
-+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
-+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
-+ }
-+ }
-+ }
-+
-+ private void removeChunkHolder(final NewChunkHolder holder) {
-+ holder.onUnload();
-+ this.autoSaveQueue.remove(holder);
-+ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
-+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
-+ }
-+
-+ // note: never call while inside the chunk system, this will absolutely break everything
-+ public void processUnloads() {
-+ TickThread.ensureTickThread("Cannot unload chunks off-main");
-+
-+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
-+ throw new IllegalStateException("Cannot unload chunks recursively");
-+ }
-+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
-+ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
-+ int unloadCountTentative = 0;
-+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
-+ final ChunkUnloadQueue.UnloadSection section
-+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
-+
-+ if (section == null) {
-+ // removed concurrently
-+ continue;
-+ }
-+
-+ // technically reading the size field is unsafe, and it may be incorrect.
-+ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible
-+ // for chunks to never unload or not unload fast enough.
-+ unloadCountTentative += section.chunks.size();
-+ }
-+
-+ if (unloadCountTentative <= 0) {
-+ // no work to do
-+ return;
-+ }
-+
-+ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed.
-+ this.processTicketUpdates();
-+
-+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05));
-+ int processedCount = 0;
-+
-+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
-+ final List<NewChunkHolder> stage1 = new ArrayList<>();
-+ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>();
-+
-+ final int sectionLowerX = sectionRef.sectionX() << sectionShift;
-+ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift;
-+
-+ // stage 1: set up for stage 2 while holding critical locks
-+ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
-+ try {
-+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
-+ try {
-+ final ChunkUnloadQueue.UnloadSection section
-+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
-+
-+ if (section == null) {
-+ // removed concurrently
-+ continue;
-+ }
-+
-+ // collect the holders to run stage 1 on
-+ final int sectionCount = section.chunks.size();
-+
-+ if ((sectionCount + processedCount) <= toUnloadCount) {
-+ // we can just drain the entire section
-+
-+ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) {
-+ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong());
-+ if (holder == null) {
-+ throw new IllegalStateException();
-+ }
-+ stage1.add(holder);
-+ }
-+
-+ // remove section
-+ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ());
-+ } else {
-+ // processedCount + len = toUnloadCount
-+ // we cannot drain the entire section
-+ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) {
-+ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong());
-+ if (holder == null) {
-+ throw new IllegalStateException();
-+ }
-+ stage1.add(holder);
-+ }
-+ }
-+
-+ // run stage 1
-+ for (int i = 0, len = stage1.size(); i < len; ++i) {
-+ final NewChunkHolder chunkHolder = stage1.get(i);
-+ chunkHolder.removeFromUnloadQueue();
-+ if (chunkHolder.isSafeToUnload() != null) {
-+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?");
-+ continue;
-+ }
-+ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1();
-+ if (state == null) {
-+ // can unload immediately
-+ this.removeChunkHolder(chunkHolder);
-+ continue;
-+ }
-+ stage2.add(state);
-+ }
-+ } finally {
-+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1
-+ final List<NewChunkHolder> stage3 = new ArrayList<>(stage2.size());
-+
-+ final Boolean before = this.blockTicketUpdates();
-+ try {
-+ for (int i = 0, len = stage2.size(); i < len; ++i) {
-+ final NewChunkHolder.UnloadState state = stage2.get(i);
-+ final NewChunkHolder holder = state.holder();
-+
-+ holder.unloadStage2(state);
-+ stage3.add(holder);
-+ }
-+ } finally {
-+ this.unblockTicketUpdates(before);
-+ }
-+
-+ // stage 3: actually attempt to remove the chunk holders
-+ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
-+ try {
-+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
-+ try {
-+ for (int i = 0, len = stage3.size(); i < len; ++i) {
-+ final NewChunkHolder holder = stage3.get(i);
-+
-+ if (holder.unloadStage3()) {
-+ this.removeChunkHolder(holder);
-+ } else {
-+ // add cooldown so the next unload check is not immediately next tick
-+ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
-+ }
-+ }
-+ } finally {
-+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
-+ }
-+ } finally {
-+ this.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ processedCount += stage1.size();
-+
-+ if (processedCount >= toUnloadCount) {
-+ break;
-+ }
-+ }
-+ }
-+
-+ public enum TicketOperationType {
-+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE
-+ }
-+
-+ public static record TicketOperation<T, V> (
-+ TicketOperationType op, long chunkCoord,
-+ TicketType<T> ticketType, int ticketLevel, T identifier,
-+ TicketType<V> ticketType2, int ticketLevel2, V identifier2
-+ ) {
-+
-+ private TicketOperation(TicketOperationType op, long chunkCoord,
-+ TicketType<T> ticketType, int ticketLevel, T identifier) {
-+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null);
-+ }
-+
-+ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
-+ }
-+
-+ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
-+ }
-+
-+ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier);
-+ }
-+
-+ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
-+ }
-+
-+ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
-+ }
-+
-+ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
-+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier);
-+ }
-+
-+ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk,
-+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
-+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
-+ return new TicketOperation<>(
-+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier,
-+ removeType, removeLevel, removeIdentifier
-+ );
-+ }
-+
-+ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk,
-+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
-+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
-+ return new TicketOperation<>(
-+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier,
-+ removeType, removeLevel, removeIdentifier
-+ );
-+ }
-+ }
-+
-+ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) {
-+ boolean ret = false;
-+ switch (operation.op) {
-+ case ADD: {
-+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
-+ break;
-+ }
-+ case REMOVE: {
-+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
-+ break;
-+ }
-+ case ADD_IF_REMOVED: {
-+ ret |= this.addIfRemovedTicket(
-+ operation.chunkCoord,
-+ operation.ticketType, operation.ticketLevel, operation.identifier,
-+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
-+ );
-+ break;
-+ }
-+ case ADD_AND_REMOVE: {
-+ ret = true;
-+ this.addAndRemoveTickets(
-+ operation.chunkCoord,
-+ operation.ticketType, operation.ticketLevel, operation.identifier,
-+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
-+ );
-+ break;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public void performTicketUpdates(final Collection<TicketOperation<?, ?>> operations) {
-+ for (final TicketOperation<?, ?> operation : operations) {
-+ this.processTicketOp(operation);
-+ }
-+ }
-+
-+ private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> {
-+ return Boolean.FALSE;
-+ });
-+
-+ public Boolean blockTicketUpdates() {
-+ final Boolean ret = BLOCK_TICKET_UPDATES.get();
-+ BLOCK_TICKET_UPDATES.set(Boolean.TRUE);
-+ return ret;
-+ }
-+
-+ public void unblockTicketUpdates(final Boolean before) {
-+ BLOCK_TICKET_UPDATES.set(before);
-+ }
-+
-+ public boolean processTicketUpdates() {
-+ return this.processTicketUpdates(true, null);
-+ }
-+
-+ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
-+
-+ static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
-+ return CURRENT_TICKET_UPDATE_SCHEDULING.get();
-+ }
-+
-+ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
-+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
-+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
-+ }
-+ if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) {
-+ TickThread.ensureTickThread("Cannot asynchronously process ticket updates");
-+ }
-+
-+ List<NewChunkHolder> changedFullStatus = null;
-+
-+ final boolean isTickThread = TickThread.isTickThread();
-+
-+ boolean ret = false;
-+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
-+ final boolean canProcessScheduling = scheduledTasks == null;
-+
-+ if (this.ticketLevelPropagator.hasPendingUpdates()) {
-+ if (scheduledTasks == null) {
-+ scheduledTasks = new ArrayList<>();
-+ }
-+ changedFullStatus = new ArrayList<>();
-+
-+ this.blockTicketUpdates();
-+ try {
-+ ret |= this.ticketLevelPropagator.performUpdates(
-+ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
-+ scheduledTasks, changedFullStatus
-+ );
-+ } finally {
-+ this.unblockTicketUpdates(Boolean.FALSE);
-+ }
-+ }
-+
-+ if (changedFullStatus != null) {
-+ this.addChangedStatuses(changedFullStatus);
-+ }
-+
-+ if (canProcessScheduling && scheduledTasks != null) {
-+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
-+ scheduledTasks.get(i).schedule();
-+ }
-+ }
-+
-+ if (canProcessFullUpdates) {
-+ ret |= this.processPendingFullUpdate();
-+ }
-+
-+ return ret;
-+ }
-+
-+ // only call on tick thread
-+ private boolean processPendingFullUpdate() {
-+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
-+
-+ boolean ret = false;
-+
-+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
-+
-+ NewChunkHolder holder;
-+ while ((holder = pendingFullLoadUpdate.poll()) != null) {
-+ ret |= holder.handleFullStatusChange(changedFullStatus);
-+
-+ if (!changedFullStatus.isEmpty()) {
-+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
-+ }
-+ changedFullStatus.clear();
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public JsonObject getDebugJson() {
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.add("unload_queue", this.unloadQueue.toDebugJson());
-+
-+ final JsonArray holders = new JsonArray();
-+ ret.add("chunkholders", holders);
-+
-+ for (final NewChunkHolder holder : this.getChunkHolders()) {
-+ holders.add(holder.getDebugJson());
-+ }
-+
-+ final JsonArray allTicketsJson = new JsonArray();
-+ ret.add("tickets", allTicketsJson);
-+
-+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator();
-+ iterator.hasNext();) {
-+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next();
-+ final long coordinate = coordinateTickets.getKey();
-+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
-+
-+ final JsonObject coordinateJson = new JsonObject();
-+ allTicketsJson.add(coordinateJson);
-+
-+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
-+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
-+
-+ final JsonArray ticketsSerialized = new JsonArray();
-+ coordinateJson.add("tickets", ticketsSerialized);
-+
-+ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating
-+ // directly over the set using the iterator
-+ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an
-+ // entry OR iterate over an entry multiple times
-+ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) {
-+ if (ticketUncasted == null) {
-+ continue;
-+ }
-+ final Ticket<?> ticket = (Ticket<?>)ticketUncasted;
-+ final JsonObject ticketSerialized = new JsonObject();
-+ ticketsSerialized.add(ticketSerialized);
-+
-+ ticketSerialized.addProperty("type", ticket.getType().toString());
-+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
-+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
-+ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay()));
-+ }
-+ }
-+
-+ return ret;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..67532b85073b7978254a0b04caadfe822679e61f
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-@@ -0,0 +1,1055 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue;
-+import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool;
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+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.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
-+import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer;
-+import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep;
-+import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonObject;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.CrashReport;
-+import net.minecraft.CrashReportCategory;
-+import net.minecraft.ReportedException;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkLevel;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.GenerationChunkHolder;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.util.StaticCache2D;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkPyramid;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.status.ChunkStep;
-+import net.minecraft.world.phys.Vec3;
-+import org.slf4j.Logger;
-+import java.io.File;
-+import java.time.LocalDateTime;
-+import java.time.format.DateTimeFormatter;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.Objects;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.Consumer;
-+
-+public final class ChunkTaskScheduler {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ public static void init(final boolean useParallelGen) {
-+ for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) {
-+ executor.setMaxParallelism(useParallelGen ? -1 : 1);
-+ }
-+
-+ LOGGER.info("Chunk system is using population gen parallelism: " + useParallelGen);
-+ }
-+
-+ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo);
-+ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong();
-+
-+ public static Long getNextChunkLoadId() {
-+ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement());
-+ }
-+
-+ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo);
-+ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong();
-+
-+ public static Long getNextNonFullLoadId() {
-+ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement());
-+ }
-+
-+ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo);
-+ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong();
-+
-+ public static Long getNextEntityLoadId() {
-+ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement());
-+ }
-+
-+ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo);
-+ private static final AtomicLong POI_LOAD_IDS = new AtomicLong();
-+
-+ public static Long getNextPoiLoadId() {
-+ return Long.valueOf(POI_LOAD_IDS.getAndIncrement());
-+ }
-+
-+ public static final TicketType<Long> CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo);
-+ private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong();
-+
-+ public static Long getNextChunkRelightId() {
-+ return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement());
-+ }
-+
-+
-+ public static int getTicketLevel(final ChunkStatus status) {
-+ return ChunkLevel.byStatus(status);
-+ }
-+
-+ public final ServerLevel world;
-+ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler;
-+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor parallelGenExecutor;
-+ private final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor radiusAwareGenExecutor;
-+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor loadExecutor;
-+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor ioExecutor;
-+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor;
-+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor;
-+
-+ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
-+
-+ public final ChunkHolderManager chunkHolderManager;
-+
-+ static {
-+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1);
-+ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2);
-+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0);
-+ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0);
-+
-+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true);
-+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true);
-+
-+ /*
-+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as
-+ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES,
-+ for a status to be parallel safe it must not read the block data from its neighbours.
-+ */
-+ final List<ChunkStatus> parallelCapableStatus = Arrays.asList(
-+ // No-op executor.
-+ ChunkStatus.EMPTY,
-+
-+ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations.
-+ // Does not touch neighbour chunks.
-+ ChunkStatus.STRUCTURE_STARTS,
-+
-+ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts
-+ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours
-+ // will not change, even if executed in parallel.
-+ ChunkStatus.STRUCTURE_REFERENCES,
-+
-+ // Safe. Mojang runs it in parallel as well.
-+ ChunkStatus.BIOMES,
-+
-+ // Safe. Mojang runs it in parallel as well.
-+ ChunkStatus.NOISE,
-+
-+ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is
-+ // completely thread-safe.
-+ ChunkStatus.SURFACE,
-+
-+ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe.
-+ ChunkStatus.CARVERS,
-+
-+ // FEATURES is not parallel safe. It writes to neighbours.
-+
-+ // no-op executor
-+ ChunkStatus.INITIALIZE_LIGHT
-+
-+ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point.
-+
-+ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe.
-+ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 -
-+ // see the task margin.
-+ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever
-+ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not
-+ // safe to execute this in parallel.
-+ // SPAWN
-+
-+ // FULL is executed on main.
-+ );
-+
-+ for (final ChunkStatus status : parallelCapableStatus) {
-+ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true);
-+ }
-+ }
-+
-+ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()];
-+ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()];
-+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()];
-+ static {
-+ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1);
-+ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1);
-+ Arrays.fill(ACCESS_RADIUS_TABLE, -1);
-+ }
-+
-+ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) {
-+ if (toStatus == ChunkStatus.EMPTY) {
-+ return 0;
-+ }
-+
-+ final ChunkStep chunkStep = pyramid.getStepTo(toStatus);
-+
-+ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY);
-+ int maxRange = radius;
-+
-+ for (int dist = 0; dist <= radius; ++dist) {
-+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist);
-+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()];
-+ if (rad == -1) {
-+ throw new IllegalStateException();
-+ }
-+
-+ maxRange = Math.max(maxRange, dist + rad);
-+ }
-+
-+ return maxRange;
-+ }
-+
-+ private static final int MAX_ACCESS_RADIUS;
-+
-+ static {
-+ final List<ChunkStatus> statuses = ChunkStatus.getStatusList();
-+ for (int i = 0, len = statuses.size(); i < len; ++i) {
-+ final ChunkStatus status = statuses.get(i);
-+ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID);
-+ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID);
-+ ACCESS_RADIUS_TABLE[i] = Math.max(
-+ ACCESS_RADIUS_TABLE_LOAD[i],
-+ ACCESS_RADIUS_TABLE_GEN[i]
-+ );
-+ }
-+ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1];
-+ }
-+
-+ public static int getMaxAccessRadius() {
-+ return MAX_ACCESS_RADIUS;
-+ }
-+
-+ public static int getAccessRadius(final ChunkStatus genStatus) {
-+ return ACCESS_RADIUS_TABLE[genStatus.getIndex()];
-+ }
-+
-+ public static int getAccessRadius(final FullChunkStatus status) {
-+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL);
-+ }
-+
-+
-+ public final ReentrantAreaLock schedulingLockArea;
-+ private final int lockShift;
-+
-+ public final int getChunkSystemLockShift() {
-+ return this.lockShift;
-+ }
-+
-+ private volatile boolean shutdown;
-+
-+ public boolean hasShutdown() {
-+ return this.shutdown;
-+ }
-+
-+ public void setShutdown(final boolean shutdown) {
-+ this.shutdown = shutdown;
-+ }
-+
-+ public ChunkTaskScheduler(final ServerLevel world) {
-+ this.world = world;
-+ // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift
-+ // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections
-+ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning
-+ // the entire section
-+ // we just take the max, as we want the smallest shift that satisfies these properties
-+ this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
-+ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift());
-+
-+ this.parallelGenExecutor = MoonriseCommon.PARALLEL_GEN_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0);
-+ this.radiusAwareGenExecutor = MoonriseCommon.RADIUS_AWARE_GROUP.createExecutor(1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0);
-+ this.loadExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0);
-+ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, 16);
-+ this.ioExecutor = MoonriseCommon.SERVER_REGION_IO_GROUP.createExecutor(-1, MoonriseCommon.IO_QUEUE_HOLD_TIME, 0);
-+ // we need a separate executor here so that on shutdown we can continue to process I/O tasks
-+ this.compressionExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0);
-+ this.saveExecutor = MoonriseCommon.LOAD_GROUP.createExecutor(-1, MoonriseCommon.WORKER_QUEUE_HOLD_TIME, 0);
-+ this.chunkHolderManager = new ChunkHolderManager(world, this);
-+ }
-+
-+ private final AtomicBoolean failedChunkSystem = new AtomicBoolean();
-+
-+ public static Object stringIfNull(final Object obj) {
-+ return obj == null ? "null" : obj;
-+ }
-+
-+ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map<String, Object> objectsOfInterest, final Throwable thr) {
-+ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr));
-+
-+ if (this.failedChunkSystem.getAndSet(true)) {
-+ return;
-+ }
-+
-+ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr));
-+
-+ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details");
-+ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString());
-+ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder));
-+ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName());
-+
-+ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest");
-+ for (final Map.Entry<String, Object> entry : objectsOfInterest.entrySet()) {
-+ if (entry.getValue() instanceof Throwable thrObject) {
-+ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject);
-+ } else {
-+ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue()));
-+ }
-+ }
-+
-+ final Runnable crash = () -> {
-+ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
-+ };
-+
-+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
-+ this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING);
-+ // so, make the main thread pick it up
-+ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
-+ }
-+
-+ public boolean executeMainThreadTask() {
-+ TickThread.ensureTickThread("Cannot execute main thread task off-main");
-+ return this.mainThreadExecutor.executeTask();
-+ }
-+
-+ public void raisePriority(final int x, final int z, final Priority priority) {
-+ this.chunkHolderManager.raisePriority(x, z, priority);
-+ }
-+
-+ public void setPriority(final int x, final int z, final Priority priority) {
-+ this.chunkHolderManager.setPriority(x, z, priority);
-+ }
-+
-+ public void lowerPriority(final int x, final int z, final Priority priority) {
-+ this.chunkHolderManager.lowerPriority(x, z, priority);
-+ }
-+
-+ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus,
-+ final boolean addTicket, final Priority priority,
-+ final Consumer<LevelChunk> onComplete) {
-+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING
-+
-+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ, Math.max(0, radius))) {
-+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
-+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+ }, priority);
-+ return;
-+ }
-+ final int accessRadius = getAccessRadius(toStatus);
-+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
-+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
-+ }
-+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
-+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
-+ }
-+
-+ if (toStatus == FullChunkStatus.INACCESSIBLE) {
-+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
-+ }
-+
-+ final int minLevel = 33 - (toStatus.ordinal() - 1);
-+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ if (addTicket) {
-+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
-+ this.chunkHolderManager.processTicketUpdates();
-+ }
-+
-+ final Consumer<LevelChunk> loadCallback = onComplete == null && !addTicket ? null : (final LevelChunk chunk) -> {
-+ try {
-+ if (onComplete != null) {
-+ onComplete.accept(chunk);
-+ }
-+ } finally {
-+ if (addTicket) {
-+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
-+ }
-+ }
-+ };
-+
-+ final boolean scheduled;
-+ final LevelChunk chunk;
-+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
-+ try {
-+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
-+ try {
-+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
-+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
-+ scheduled = false;
-+ chunk = null;
-+ } else {
-+ final FullChunkStatus currStatus = chunkHolder.getChunkStatus();
-+ if (currStatus.isOrAfter(toStatus)) {
-+ scheduled = false;
-+ chunk = (LevelChunk)chunkHolder.getCurrentChunk();
-+ } else {
-+ scheduled = true;
-+ chunk = null;
-+
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ final NewChunkHolder neighbour =
-+ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ);
-+ if (neighbour != null) {
-+ neighbour.raisePriority(priority);
-+ }
-+ }
-+ }
-+
-+ // ticket level should schedule for us
-+ if (loadCallback != null) {
-+ chunkHolder.addFullStatusConsumer(toStatus, loadCallback);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } finally {
-+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ if (loadCallback != null && !scheduled) {
-+ // couldn't schedule
-+ try {
-+ loadCallback.accept(chunk);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to process chunk full status callback", thr);
-+ }
-+ }
-+ }
-+
-+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket,
-+ final Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ if (gen) {
-+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+ return;
-+ }
-+ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
-+ if (chunk == null) {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ } else {
-+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
-+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+ } else {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ }
-+ }
-+ });
-+ }
-+
-+ // only appropriate to use with syncLoadNonFull
-+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-+ final Priority priority) {
-+ final int accessRadius = getAccessRadius(toStatus);
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
-+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
-+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
-+ try {
-+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
-+ try {
-+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
-+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
-+ return false;
-+ } else {
-+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
-+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
-+ return true;
-+ } else {
-+ chunkHolder.raisePriority(priority);
-+
-+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
-+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } finally {
-+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ for (int i = 0, len = tasks.size(); i < len; ++i) {
-+ tasks.get(i).schedule();
-+ }
-+
-+ return true;
-+ }
-+
-+ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only
-+ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock
-+ // between regions
-+ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
-+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
-+ throw new IllegalArgumentException("Status: " + status);
-+ }
-+
-+ if (!TickThread.isTickThread()) {
-+ return this.world.getChunkSource().getChunk(chunkX, chunkZ, status, true);
-+ }
-+
-+ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
-+ if (loaded != null) {
-+ return loaded;
-+ }
-+
-+ if (this.hasShutdown()) {
-+ throw new IllegalStateException(
-+ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.world) + "' at "
-+ + "(" + chunkX + "," + chunkZ + ") status: " + status
-+ );
-+ }
-+
-+ final Long ticketId = getNextNonFullLoadId();
-+ final int ticketLevel = getTicketLevel(status);
-+ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
-+ this.chunkHolderManager.processTicketUpdates();
-+
-+ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, Priority.BLOCKING);
-+
-+ // we could do a simple spinwait here, since we do not need to process tasks while performing this load
-+ // but we process tasks only because it's a better use of the time spent
-+ this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> {
-+ return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null;
-+ });
-+
-+ loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
-+
-+ this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
-+
-+ if (loaded == null) {
-+ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
-+ }
-+
-+ return loaded;
-+ }
-+
-+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
-+ final Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) {
-+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
-+ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-+ }, priority);
-+ return;
-+ }
-+ final int accessRadius = getAccessRadius(toStatus);
-+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
-+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
-+ }
-+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
-+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
-+ }
-+
-+ if (toStatus == ChunkStatus.FULL) {
-+ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete);
-+ return;
-+ }
-+
-+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
-+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ if (addTicket) {
-+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
-+ this.chunkHolderManager.processTicketUpdates();
-+ }
-+
-+ final Consumer<ChunkAccess> loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> {
-+ try {
-+ if (onComplete != null) {
-+ onComplete.accept(chunk);
-+ }
-+ } finally {
-+ if (addTicket) {
-+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
-+ }
-+ }
-+ };
-+
-+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
-+
-+ final boolean scheduled;
-+ final ChunkAccess chunk;
-+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
-+ try {
-+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
-+ try {
-+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
-+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
-+ scheduled = false;
-+ chunk = null;
-+ } else {
-+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
-+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
-+ scheduled = false;
-+ chunk = chunkHolder.getCurrentChunk();
-+ } else {
-+ scheduled = true;
-+ chunk = null;
-+ chunkHolder.raisePriority(priority);
-+
-+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
-+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
-+ }
-+ if (loadCallback != null) {
-+ chunkHolder.addStatusConsumer(toStatus, loadCallback);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } finally {
-+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
-+ }
-+
-+ for (int i = 0, len = tasks.size(); i < len; ++i) {
-+ tasks.get(i).schedule();
-+ }
-+
-+ if (loadCallback != null && !scheduled) {
-+ // couldn't schedule
-+ try {
-+ loadCallback.accept(chunk);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to process chunk status callback", thr);
-+ }
-+ }
-+ }
-+
-+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk,
-+ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours,
-+ final ChunkStatus toStatus, final Priority initialPriority) {
-+ if (toStatus == ChunkStatus.EMPTY) {
-+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
-+ }
-+ if (toStatus == ChunkStatus.LIGHT) {
-+ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority);
-+ }
-+ if (toStatus == ChunkStatus.FULL) {
-+ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority);
-+ }
-+
-+ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority);
-+ }
-+
-+ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder,
-+ final List<ChunkProgressionTask> allTasks) {
-+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL));
-+ }
-+
-+ // rets new task scheduled for the _specified_ chunk
-+ // note: this must hold the scheduling lock
-+ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done
-+ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed!
-+ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus,
-+ final NewChunkHolder chunkHolder, final List<ChunkProgressionTask> allTasks,
-+ final Priority minPriority) {
-+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) {
-+ throw new IllegalStateException("Not holding scheduling lock");
-+ }
-+
-+ if (chunkHolder.hasGenerationTask()) {
-+ chunkHolder.upgradeGenTarget(targetStatus);
-+ return null;
-+ }
-+
-+ final Priority requestedPriority = Priority.max(
-+ minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL)
-+ );
-+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
-+ final ChunkAccess chunk = chunkHolder.getCurrentChunk();
-+
-+ if (currentGenStatus == null) {
-+ // not yet loaded
-+ final ChunkProgressionTask task = this.createTask(
-+ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority
-+ );
-+
-+ allTasks.add(task);
-+
-+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>(1);
-+ chunkHolderNeighbours.add(chunkHolder);
-+
-+ chunkHolder.setGenerationTarget(targetStatus);
-+ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours);
-+
-+ return task;
-+ }
-+
-+ if (currentGenStatus.isOrAfter(targetStatus)) {
-+ // nothing to do
-+ return null;
-+ }
-+
-+ // we know for sure now that we want to schedule _something_, so set the target
-+ chunkHolder.setGenerationTarget(targetStatus);
-+
-+ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus();
-+ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus();
-+ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID;
-+ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus);
-+
-+ final int neighbourReadRadius = Math.max(
-+ 0,
-+ chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY)
-+ );
-+
-+ boolean unGeneratedNeighbours = false;
-+
-+ if (neighbourReadRadius > 0) {
-+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap;
-+ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) {
-+ final int x = CoordinateUtils.getChunkX(pos);
-+ final int z = CoordinateUtils.getChunkZ(pos);
-+ final int radius = Math.max(Math.abs(x), Math.abs(z));
-+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius);
-+
-+ unGeneratedNeighbours |= this.checkNeighbour(
-+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
-+ );
-+ }
-+ }
-+
-+ if (unGeneratedNeighbours) {
-+ // can't schedule, but neighbour completion will schedule for us when they're ALL done
-+
-+ // propagate our priority to neighbours
-+ chunkHolder.recalculateNeighbourPriorities();
-+ return null;
-+ }
-+
-+ // need to gather neighbours
-+
-+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
-+ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D
-+ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> {
-+ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz);
-+ chunkHolderNeighbours.add(holder);
-+
-+ return holder.vanillaChunkHolder;
-+ });
-+
-+ final ChunkProgressionTask task = this.createTask(
-+ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
-+ chunkHolder.getEffectivePriority(Priority.NORMAL)
-+ );
-+ allTasks.add(task);
-+
-+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours);
-+
-+ return task;
-+ }
-+
-+ // rets true if the neighbour is not at the required status, false otherwise
-+ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center,
-+ final List<ChunkProgressionTask> tasks, final Priority minPriority) {
-+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+
-+ if (chunkHolder == null) {
-+ throw new IllegalStateException("Missing chunkholder when required");
-+ }
-+
-+ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus();
-+ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) {
-+ return false;
-+ }
-+
-+ if (chunkHolder.hasFailedGeneration()) {
-+ return true;
-+ }
-+
-+ center.addGenerationBlockingNeighbour(chunkHolder);
-+ chunkHolder.addWaitingNeighbour(center, requiredStatus);
-+
-+ if (chunkHolder.upgradeGenTarget(requiredStatus)) {
-+ return true;
-+ }
-+
-+ // not at status required, so we need to schedule its generation
-+ this.schedule(
-+ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority
-+ );
-+
-+ return true;
-+ }
-+
-+ /**
-+ * @deprecated Chunk tasks must be tied to coordinates in the future
-+ */
-+ @Deprecated
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
-+ return this.scheduleChunkTask(run, Priority.NORMAL);
-+ }
-+
-+ /**
-+ * @deprecated Chunk tasks must be tied to coordinates in the future
-+ */
-+ @Deprecated
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) {
-+ return this.mainThreadExecutor.queueTask(run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
-+ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-+ final Priority priority) {
-+ return this.mainThreadExecutor.createTask(run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
-+ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-+ final Priority priority) {
-+ return this.mainThreadExecutor.queueTask(run, priority);
-+ }
-+
-+ public boolean halt(final boolean sync, final long maxWaitNS) {
-+ this.radiusAwareGenExecutor.halt();
-+ this.parallelGenExecutor.halt();
-+ this.loadExecutor.halt();
-+ if (sync) {
-+ final long time = System.nanoTime();
-+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
-+ if (
-+ !this.radiusAwareGenExecutor.isActive() &&
-+ !this.parallelGenExecutor.isActive() &&
-+ !this.loadExecutor.isActive()
-+ ) {
-+ return true;
-+ }
-+ if ((System.nanoTime() - time) >= maxWaitNS) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public boolean haltIO(final boolean sync, final long maxWaitNS) {
-+ this.ioExecutor.halt();
-+ this.saveExecutor.halt();
-+ this.compressionExecutor.halt();
-+ if (sync) {
-+ final long time = System.nanoTime();
-+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
-+ if (
-+ !this.ioExecutor.isActive() &&
-+ !this.saveExecutor.isActive() &&
-+ !this.compressionExecutor.isActive()
-+ ) {
-+ return true;
-+ }
-+ if ((System.nanoTime() - time) >= maxWaitNS) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack
-+
-+ public static final class ChunkInfo {
-+
-+ public final int chunkX;
-+ public final int chunkZ;
-+ public final ServerLevel world;
-+
-+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) {
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.world = world;
-+ }
-+
-+ public JsonObject toJson() {
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.addProperty("chunk-x", Integer.valueOf(this.chunkX));
-+ ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ));
-+ ret.addProperty("world-name", WorldUtil.getWorldName(this.world));
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']";
-+ }
-+ }
-+
-+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) {
-+ synchronized (WAITING_CHUNKS) {
-+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
-+ }
-+ }
-+
-+ public static void popChunkWait() {
-+ synchronized (WAITING_CHUNKS) {
-+ WAITING_CHUNKS.pop();
-+ }
-+ }
-+
-+ public static ChunkInfo[] getChunkInfos() {
-+ synchronized (WAITING_CHUNKS) {
-+ return WAITING_CHUNKS.toArray(new ChunkInfo[0]);
-+ }
-+ }
-+
-+ private static JsonObject debugPlayer(final ServerPlayer player) {
-+ final Level world = player.level();
-+
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.addProperty("name", player.getScoreboardName());
-+ ret.addProperty("uuid", player.getUUID().toString());
-+ ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer());
-+
-+ ret.addProperty("world-name", WorldUtil.getWorldName(world));
-+
-+ final Vec3 pos = player.position();
-+
-+ ret.addProperty("x", pos.x);
-+ ret.addProperty("y", pos.y);
-+ ret.addProperty("z", pos.z);
-+
-+ final Entity.RemovalReason removalReason = player.getRemovalReason();
-+
-+ ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name());
-+
-+ ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson());
-+
-+ return ret;
-+ }
-+
-+ public JsonObject getDebugJson() {
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift()));
-+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT));
-+ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift()));
-+
-+ ret.addProperty("name", WorldUtil.getWorldName(this.world));
-+ ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance());
-+ ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance());
-+ ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance());
-+
-+ final JsonArray players = new JsonArray();
-+ ret.add("players", players);
-+
-+ for (final ServerPlayer player : this.world.players()) {
-+ players.add(debugPlayer(player));
-+ }
-+
-+ ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson());
-+
-+ return ret;
-+ }
-+
-+ public static JsonObject debugAllWorlds(final MinecraftServer server) {
-+ final JsonObject ret = new JsonObject();
-+
-+ ret.addProperty("data-version", 2);
-+
-+ final JsonArray allPlayers = new JsonArray();
-+ ret.add("all-players", allPlayers);
-+
-+ for (final ServerPlayer player : server.getPlayerList().getPlayers()) {
-+ allPlayers.add(debugPlayer(player));
-+ }
-+
-+ final JsonArray chunkWaitInfos = new JsonArray();
-+ ret.add("chunk-wait-infos", chunkWaitInfos);
-+
-+ for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) {
-+ chunkWaitInfos.add(info.toJson());
-+ }
-+
-+ final JsonArray worlds = new JsonArray();
-+ ret.add("worlds", worlds);
-+
-+ for (final ServerLevel world : server.getAllLevels()) {
-+ worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson());
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static File getChunkDebugFile() {
-+ return new File(
-+ new File(new File("."), "debug"),
-+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"
-+ );
-+ }
-+
-+ public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) {
-+ final ChunkInfo[] chunkInfos = getChunkInfos();
-+ if (chunkInfos.length > 0) {
-+ LOGGER.error("Chunk wait task info below: ");
-+ for (final ChunkInfo chunkInfo : chunkInfos) {
-+ final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
-+ LOGGER.error("Chunk wait: " + chunkInfo);
-+ LOGGER.error("Chunk holder: " + holder);
-+ }
-+
-+ if (writeDebugInfo) {
-+ final File file = getChunkDebugFile();
-+ LOGGER.error("Writing chunk information dump to " + file);
-+ try {
-+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file);
-+ LOGGER.error("Successfully written chunk information!");
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr);
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..eafa4e6d55cd0f9314ac0f2b96a7f48fbb5e1a4c
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-@@ -0,0 +1,1998 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
-+import ca.spottedleaf.concurrentutil.executor.Cancellable;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.common.misc.LazyRunnable;
-+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.common.util.ChunkSystem;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonElement;
-+import com.google.gson.JsonNull;
-+import com.google.gson.JsonObject;
-+import com.google.gson.JsonPrimitive;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkLevel;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.progress.ChunkProgressListener;
-+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.ImposterProtoChunk;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayList;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.Objects;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.function.Consumer;
-+
-+public final class NewChunkHolder {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class);
-+
-+ public final ChunkData holderData;
-+
-+ public final ServerLevel world;
-+ public final int chunkX;
-+ public final int chunkZ;
-+
-+ public final ChunkTaskScheduler scheduler;
-+
-+ // load/unload state
-+
-+ // chunk data state
-+
-+ private ChunkEntitySlices entityChunk;
-+ // entity chunk that is loaded, but not yet deserialized
-+ private CompoundTag pendingEntityChunk;
-+
-+ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) {
-+ 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);
-+ try {
-+ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) {
-+ return this.entityChunk;
-+ }
-+ final CompoundTag pendingEntityChunk = this.pendingEntityChunk;
-+ if (!transientChunk && pendingEntityChunk == null) {
-+ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!");
-+ }
-+
-+ if (this.entityChunk == null) {
-+ ret = this.entityChunk = new ChunkEntitySlices(
-+ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(),
-+ this.holderData, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ );
-+
-+ ret.setTransient(transientChunk);
-+
-+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret);
-+ } else {
-+ // transientChunk = false here
-+ ret = this.entityChunk;
-+ this.entityChunk.setTransient(false);
-+ }
-+
-+ if (!transientChunk) {
-+ this.pendingEntityChunk = null;
-+ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk;
-+ } else {
-+ entityChunk = null;
-+ }
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ if (!transientChunk) {
-+ if (entityChunk != null) {
-+ final List<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk);
-+
-+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ));
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_
-+ // been read from disk
-+ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag();
-+
-+ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask;
-+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
-+ // then the task is rescheduled
-+ private List<GenericDataLoadTaskCallback> entityDataLoadTaskWaiters;
-+
-+ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() {
-+ return this.entityDataLoadTask;
-+ }
-+
-+ // must hold schedule lock for the two below functions
-+
-+ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized
-+ // or added into the world (or even into entityChunk)
-+ public boolean isEntityChunkNBTLoaded() {
-+ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null;
-+ }
-+
-+ private void completeEntityLoad(final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) {
-+ final List<GenericDataLoadTaskCallback> completeWaiters;
-+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null;
-+ boolean scheduleEntityTask = false;
-+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ final List<GenericDataLoadTaskCallback> waiters = this.entityDataLoadTaskWaiters;
-+ this.entityDataLoadTask = null;
-+ if (result != null) {
-+ this.entityDataLoadTaskWaiters = null;
-+ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left();
-+ if (result.right() != null) {
-+ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right());
-+ }
-+
-+ for (final GenericDataLoadTaskCallback callback : waiters) {
-+ callback.markCompleted();
-+ }
-+
-+ completeWaiters = waiters;
-+ } else {
-+ // cancelled
-+ completeWaiters = null;
-+
-+ // need to re-schedule?
-+ if (waiters.isEmpty()) {
-+ this.entityDataLoadTaskWaiters = null;
-+ // no tasks to schedule _for_
-+ } else {
-+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
-+ );
-+ entityDataLoadTask.addCallback(this::completeEntityLoad);
-+ // need one schedule() per waiter
-+ for (final GenericDataLoadTaskCallback callback : waiters) {
-+ scheduleEntityTask |= entityDataLoadTask.schedule(true);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ if (scheduleEntityTask) {
-+ entityDataLoadTask.scheduleNow();
-+ }
-+
-+ // avoid holding the scheduling lock while completing
-+ if (completeWaiters != null) {
-+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
-+ callback.acceptCompleted(result);
-+ }
-+ }
-+
-+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ this.checkUnload();
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+
-+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
-+ // however, when the consumer is invoked, it will hold the schedule lock
-+ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer<GenericDataLoadTask.TaskResult<CompoundTag, Throwable>> consumer) {
-+ if (this.isEntityChunkNBTLoaded()) {
-+ throw new IllegalStateException("Cannot load entity data, it is already loaded");
-+ }
-+ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this!
-+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
-+ throw new IllegalStateException("Must hold scheduling lock");
-+ }
-+
-+ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this);
-+
-+ if (this.entityDataLoadTask == null) {
-+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
-+ );
-+ this.entityDataLoadTask.addCallback(this::completeEntityLoad);
-+ this.entityDataLoadTaskWaiters = new ArrayList<>();
-+ }
-+ this.entityDataLoadTaskWaiters.add(ret);
-+ if (this.entityDataLoadTask.schedule(true)) {
-+ ret.schedule = this.entityDataLoadTask;
-+ }
-+ this.checkUnload();
-+
-+ return ret;
-+ }
-+
-+ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback {
-+
-+ public EntityDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
-+ super(consumer, chunkHolder);
-+ }
-+
-+ @Override
-+ void internalCancel() {
-+ this.chunkHolder.entityDataLoadTaskWaiters.remove(this);
-+ this.chunkHolder.entityDataLoadTask.cancel();
-+ }
-+ }
-+
-+ private PoiChunk poiChunk;
-+
-+ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask;
-+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
-+ // then the task is rescheduled
-+ private List<GenericDataLoadTaskCallback> poiDataLoadTaskWaiters;
-+
-+ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() {
-+ return this.poiDataLoadTask;
-+ }
-+
-+ // must hold schedule lock for the two below functions
-+
-+ public boolean isPoiChunkLoaded() {
-+ return this.poiChunk != null;
-+ }
-+
-+ private void completePoiLoad(final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) {
-+ final List<GenericDataLoadTaskCallback> completeWaiters;
-+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null;
-+ boolean schedulePoiTask = false;
-+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ final List<GenericDataLoadTaskCallback> waiters = this.poiDataLoadTaskWaiters;
-+ this.poiDataLoadTask = null;
-+ if (result != null) {
-+ this.poiDataLoadTaskWaiters = null;
-+ this.poiChunk = result.left();
-+ if (result.right() != null) {
-+ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right());
-+ }
-+
-+ for (final GenericDataLoadTaskCallback callback : waiters) {
-+ callback.markCompleted();
-+ }
-+
-+ completeWaiters = waiters;
-+ } else {
-+ // cancelled
-+ completeWaiters = null;
-+
-+ // need to re-schedule?
-+ if (waiters.isEmpty()) {
-+ this.poiDataLoadTaskWaiters = null;
-+ // no tasks to schedule _for_
-+ } else {
-+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
-+ );
-+ poiDataLoadTask.addCallback(this::completePoiLoad);
-+ // need one schedule() per waiter
-+ for (final GenericDataLoadTaskCallback callback : waiters) {
-+ schedulePoiTask |= poiDataLoadTask.schedule(true);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ if (schedulePoiTask) {
-+ poiDataLoadTask.scheduleNow();
-+ }
-+
-+ // avoid holding the scheduling lock while completing
-+ if (completeWaiters != null) {
-+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
-+ callback.acceptCompleted(result);
-+ }
-+ }
-+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ this.checkUnload();
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+
-+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
-+ // however, when the consumer is invoked, it will hold the schedule lock
-+ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer<GenericDataLoadTask.TaskResult<PoiChunk, Throwable>> consumer) {
-+ if (this.isPoiChunkLoaded()) {
-+ throw new IllegalStateException("Cannot load poi data, it is already loaded");
-+ }
-+ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this!
-+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
-+ throw new IllegalStateException("Must hold scheduling lock");
-+ }
-+
-+ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this);
-+
-+ if (this.poiDataLoadTask == null) {
-+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
-+ );
-+ this.poiDataLoadTask.addCallback(this::completePoiLoad);
-+ this.poiDataLoadTaskWaiters = new ArrayList<>();
-+ }
-+ this.poiDataLoadTaskWaiters.add(ret);
-+ if (this.poiDataLoadTask.schedule(true)) {
-+ ret.schedule = this.poiDataLoadTask;
-+ }
-+ this.checkUnload();
-+
-+ return ret;
-+ }
-+
-+ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback {
-+
-+ public PoiDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
-+ super(consumer, chunkHolder);
-+ }
-+
-+ @Override
-+ void internalCancel() {
-+ this.chunkHolder.poiDataLoadTaskWaiters.remove(this);
-+ this.chunkHolder.poiDataLoadTask.cancel();
-+ }
-+ }
-+
-+ public static abstract class GenericDataLoadTaskCallback implements Cancellable {
-+
-+ protected final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer;
-+ protected final NewChunkHolder chunkHolder;
-+ protected boolean completed;
-+ protected GenericDataLoadTask<?, ?> schedule;
-+ protected final AtomicBoolean scheduled = new AtomicBoolean();
-+
-+ public GenericDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer,
-+ final NewChunkHolder chunkHolder) {
-+ this.consumer = consumer;
-+ this.chunkHolder = chunkHolder;
-+ }
-+
-+ public void schedule() {
-+ if (this.scheduled.getAndSet(true)) {
-+ throw new IllegalStateException("Double calling schedule()");
-+ }
-+ if (this.schedule != null) {
-+ this.schedule.scheduleNow();
-+ this.schedule = null;
-+ }
-+ }
-+
-+ boolean isCompleted() {
-+ return this.completed;
-+ }
-+
-+ // must hold scheduling lock
-+ private boolean setCompleted() {
-+ if (this.completed) {
-+ return false;
-+ }
-+ return this.completed = true;
-+ }
-+
-+ // must hold scheduling lock
-+ void markCompleted() {
-+ if (this.completed) {
-+ throw new IllegalStateException("May not be completed here");
-+ }
-+ this.completed = true;
-+ }
-+
-+ void acceptCompleted(final GenericDataLoadTask.TaskResult<?, Throwable> result) {
-+ if (result != null) {
-+ if (this.completed) {
-+ this.consumer.accept(result);
-+ } else {
-+ throw new IllegalStateException("Cannot be uncompleted at this point");
-+ }
-+ } else {
-+ throw new NullPointerException("Result cannot be null (cancelled)");
-+ }
-+ }
-+
-+ // holds scheduling lock
-+ abstract void internalCancel();
-+
-+ @Override
-+ public boolean cancel() {
-+ final NewChunkHolder holder = this.chunkHolder;
-+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ);
-+ try {
-+ if (!this.completed) {
-+ this.completed = true;
-+ this.internalCancel();
-+ return true;
-+ }
-+ return false;
-+ } finally {
-+ holder.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+ }
-+
-+ private ChunkAccess currentChunk;
-+
-+ // generation status state
-+
-+ /**
-+ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all
-+ */
-+ private ChunkStatus currentGenStatus;
-+
-+ // This allows lockless access to the chunk and last gen status
-+ private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]);
-+
-+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {};
-+ private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class);
-+ private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length];
-+
-+ private volatile ChunkCompletion lastChunkCompletion;
-+
-+ public ChunkCompletion getLastChunkCompletion() {
-+ return this.lastChunkCompletion;
-+ }
-+
-+ public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) {
-+ final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex());
-+ return completion == null ? null : completion.chunk;
-+ }
-+
-+ public ChunkAccess getChunkIfPresent(final ChunkStatus status) {
-+ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel());
-+
-+ if (maxStatus == null || status.isAfter(maxStatus)) {
-+ return null;
-+ }
-+
-+ return this.getChunkIfPresentUnchecked(status);
-+ }
-+
-+ public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) {
-+ for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) {
-+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i]));
-+ }
-+ }
-+
-+ /**
-+ * The target final chunk status the chunk system will bring the chunk to.
-+ */
-+ private ChunkStatus requestedGenStatus;
-+
-+ private ChunkProgressionTask generationTask;
-+ private ChunkStatus generationTaskStatus;
-+
-+ /**
-+ * contains the neighbours that this chunk generation is blocking on
-+ */
-+ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4);
-+
-+ /**
-+ * map of ChunkHolder -> Required Status for this chunk
-+ */
-+ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>();
-+
-+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) {
-+ this.neighboursBlockingGenTask.add(neighbour);
-+ }
-+
-+ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) {
-+ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty();
-+ this.neighboursWaitingForUs.put(neighbour, requiredStatus);
-+ if (wasEmpty) {
-+ this.checkUnload();
-+ }
-+ }
-+
-+ // priority state
-+
-+ // the target priority for this chunk to generate at
-+ private Priority priority = null;
-+ private boolean priorityLocked;
-+
-+ // the priority neighbouring chunks have requested this chunk generate at
-+ private Priority neighbourRequestedPriority = null;
-+
-+ public Priority getEffectivePriority(final Priority dfl) {
-+ final Priority neighbour = this.neighbourRequestedPriority;
-+ final Priority us = this.priority;
-+
-+ if (neighbour == null) {
-+ return us == null ? dfl : us;
-+ }
-+ if (us == null) {
-+ return neighbour;
-+ }
-+
-+ return Priority.max(us, neighbour);
-+ }
-+
-+ private void recalculateNeighbourRequestedPriority() {
-+ if (this.neighboursWaitingForUs.isEmpty()) {
-+ this.neighbourRequestedPriority = null;
-+ return;
-+ }
-+
-+ Priority max = null;
-+
-+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
-+ final Priority neighbourPriority = holder.getEffectivePriority(null);
-+ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
-+ max = neighbourPriority;
-+ }
-+ }
-+
-+ final Priority current = this.getEffectivePriority(Priority.NORMAL);
-+ this.neighbourRequestedPriority = max;
-+ final Priority next = this.getEffectivePriority(Priority.NORMAL);
-+
-+ if (current == next) {
-+ return;
-+ }
-+
-+ // our effective priority has changed, so change our task
-+ if (this.generationTask != null) {
-+ this.generationTask.setPriority(next);
-+ }
-+
-+ // now propagate this to our neighbours
-+ this.recalculateNeighbourPriorities();
-+ }
-+
-+ public void recalculateNeighbourPriorities() {
-+ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) {
-+ holder.recalculateNeighbourRequestedPriority();
-+ }
-+ }
-+
-+ // must hold scheduling lock
-+ public void raisePriority(final Priority priority) {
-+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) {
-+ return;
-+ }
-+ this.setPriority(priority);
-+ }
-+
-+ private void lockPriority() {
-+ this.priority = null;
-+ this.priorityLocked = true;
-+ }
-+
-+ // must hold scheduling lock
-+ public void setPriority(final Priority priority) {
-+ if (this.priorityLocked) {
-+ return;
-+ }
-+ final Priority old = this.getEffectivePriority(null);
-+ this.priority = priority;
-+ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL);
-+
-+ if (old != newPriority) {
-+ if (this.generationTask != null) {
-+ this.generationTask.setPriority(newPriority);
-+ }
-+ }
-+
-+ this.recalculateNeighbourPriorities();
-+ }
-+
-+ // must hold scheduling lock
-+ public void lowerPriority(final Priority priority) {
-+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) {
-+ return;
-+ }
-+ this.setPriority(priority);
-+ }
-+
-+ // error handling state
-+ private ChunkStatus failedGenStatus;
-+ private Throwable genTaskException;
-+ private Thread genTaskFailedThread;
-+
-+ private boolean failedLightUpdate;
-+
-+ public void failedLightUpdate() {
-+ this.failedLightUpdate = true;
-+ }
-+
-+ public boolean hasFailedGeneration() {
-+ return this.genTaskException != null;
-+ }
-+
-+ // ticket level state
-+ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
-+ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
-+
-+ public int getTicketLevel() {
-+ return this.currentTicketLevel;
-+ }
-+
-+ public final ChunkHolder vanillaChunkHolder;
-+
-+ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) {
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.scheduler = scheduler;
-+ this.vanillaChunkHolder = new ChunkHolder(
-+ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world,
-+ world.getLightEngine(), null, world.getChunkSource().chunkMap
-+ );
-+ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this);
-+ this.holderData = ((ChunkSystemLevel)this.world).moonrise$requestChunkData(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ public ChunkAccess getCurrentChunk() {
-+ return this.currentChunk;
-+ }
-+
-+ int getCurrentTicketLevel() {
-+ return this.currentTicketLevel;
-+ }
-+
-+ void updateTicketLevel(final int toLevel) {
-+ this.currentTicketLevel = toLevel;
-+ }
-+
-+ private int totalNeighboursUsingThisChunk = 0;
-+
-+ // holds schedule lock
-+ public void addNeighbourUsingChunk() {
-+ final int now = ++this.totalNeighboursUsingThisChunk;
-+
-+ if (now == 1) {
-+ this.checkUnload();
-+ }
-+ }
-+
-+ // holds schedule lock
-+ public void removeNeighbourUsingChunk() {
-+ final int now = --this.totalNeighboursUsingThisChunk;
-+
-+ if (now == 0) {
-+ this.checkUnload();
-+ }
-+
-+ if (now < 0) {
-+ throw new IllegalStateException("Neighbours using this chunk cannot be negative");
-+ }
-+ }
-+
-+ // must hold scheduling lock
-+ // returns string reason for why chunk should remain loaded, null otherwise
-+ public final String isSafeToUnload() {
-+ // is ticket level below threshold?
-+ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) {
-+ return "ticket_level";
-+ }
-+
-+ // are we being used by another chunk for generation?
-+ if (this.totalNeighboursUsingThisChunk != 0) {
-+ return "neighbours_generating";
-+ }
-+
-+ // are we going to be used by another chunk for generation?
-+ if (!this.neighboursWaitingForUs.isEmpty()) {
-+ return "neighbours_waiting";
-+ }
-+
-+ // chunk must be marked inaccessible (i.e. unloaded to plugins)
-+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) {
-+ return "fullchunkstatus";
-+ }
-+
-+ // are we currently generating anything, or have requested generation?
-+ if (this.generationTask != null) {
-+ return "generating";
-+ }
-+ if (this.requestedGenStatus != null) {
-+ return "requested_generation";
-+ }
-+
-+ // entity data requested?
-+ if (this.entityDataLoadTask != null) {
-+ return "entity_data_requested";
-+ }
-+
-+ // poi data requested?
-+ if (this.poiDataLoadTask != null) {
-+ return "poi_data_requested";
-+ }
-+
-+ // are we pending serialization?
-+ if (this.entityDataUnload != null) {
-+ return "entity_serialization";
-+ }
-+ if (this.poiDataUnload != null) {
-+ return "poi_serialization";
-+ }
-+ if (this.chunkDataUnload != null) {
-+ return "chunk_serialization";
-+ }
-+
-+ // Note: light tasks do not need a check, as they add a ticket.
-+
-+ // nothing is using this chunk, so it should be unloaded
-+ return null;
-+ }
-+
-+ /** Unloaded from chunk map */
-+ private boolean unloaded;
-+
-+ void onUnload() {
-+ this.unloaded = true;
-+ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
-+ }
-+
-+ private boolean inUnloadQueue = false;
-+
-+ void removeFromUnloadQueue() {
-+ this.inUnloadQueue = false;
-+ }
-+
-+ // must hold scheduling lock
-+ private void checkUnload() {
-+ if (this.unloaded) {
-+ return;
-+ }
-+ if (this.isSafeToUnload() == null) {
-+ // ensure in unload queue
-+ if (!this.inUnloadQueue) {
-+ this.inUnloadQueue = true;
-+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ);
-+ }
-+ } else {
-+ // ensure not in unload queue
-+ if (this.inUnloadQueue) {
-+ this.inUnloadQueue = false;
-+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ);
-+ }
-+ }
-+ }
-+
-+ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {};
-+
-+ // note: these are completed with null to indicate that no write occurred
-+ // they are also completed with null to indicate a null write occurred
-+ private UnloadTask chunkDataUnload;
-+ private UnloadTask entityDataUnload;
-+ private UnloadTask poiDataUnload;
-+
-+ public static final record UnloadTask(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task,
-+ LazyRunnable toRun) {}
-+
-+ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
-+ switch (type) {
-+ case CHUNK_DATA:
-+ return this.chunkDataUnload;
-+ case ENTITY_DATA:
-+ return this.entityDataUnload;
-+ case POI_DATA:
-+ return this.poiDataUnload;
-+ default:
-+ throw new IllegalStateException("Unknown regionfile type " + type);
-+ }
-+ }
-+
-+ private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
-+ switch (type) {
-+ case CHUNK_DATA: {
-+ this.chunkDataUnload = null;
-+ return;
-+ }
-+ case ENTITY_DATA: {
-+ this.entityDataUnload = null;
-+ return;
-+ }
-+ case POI_DATA: {
-+ this.poiDataUnload = null;
-+ return;
-+ }
-+ default:
-+ throw new IllegalStateException("Unknown regionfile type " + type);
-+ }
-+ }
-+
-+ private UnloadState unloadState;
-+
-+ // holds schedule lock
-+ UnloadState unloadStage1() {
-+ // because we hold the scheduling lock, we cannot actually unload anything
-+ // so, what we do here instead is to null this chunk's state and setup the unload tasks
-+ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which
-+ // we do not hold the lock) c
-+ final ChunkAccess chunk = this.currentChunk;
-+ final ChunkEntitySlices entityChunk = this.entityChunk;
-+ final PoiChunk poiChunk = this.poiChunk;
-+ // chunk state
-+ this.currentChunk = null;
-+ this.currentGenStatus = null;
-+ for (int i = 0; i < this.chunkCompletions.length; ++i) {
-+ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null);
-+ }
-+ this.lastChunkCompletion = null;
-+ // entity chunk state
-+ this.entityChunk = null;
-+ this.pendingEntityChunk = null;
-+
-+ // poi chunk state
-+ this.poiChunk = null;
-+
-+ // priority state
-+ this.priorityLocked = false;
-+
-+ if (chunk != null) {
-+ final LazyRunnable toRun = new LazyRunnable();
-+ this.chunkDataUnload = new UnloadTask(new CallbackCompletable<>(), this.scheduler.saveExecutor.createTask(toRun), toRun);
-+ }
-+ if (poiChunk != null) {
-+ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null);
-+ }
-+ if (entityChunk != null) {
-+ this.entityDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null);
-+ }
-+
-+ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null;
-+ }
-+
-+ // data is null if failed or does not need to be saved
-+ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) {
-+ if (data != null) {
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
-+ }
-+
-+ this.getUnloadTask(type).completable().complete(data);
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ // can only write to these fields while holding the schedule lock
-+ this.removeUnloadTask(type);
-+ this.checkUnload();
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+
-+ void unloadStage2(final UnloadState state) {
-+ this.unloadState = null;
-+ final ChunkAccess chunk = state.chunk();
-+ final ChunkEntitySlices entityChunk = state.entityChunk();
-+ final PoiChunk poiChunk = state.poiChunk();
-+
-+ final boolean shouldLevelChunkNotSave = PlatformHooks.get().forceNoSave(chunk);
-+
-+ // unload chunk data
-+ if (chunk != null) {
-+ if (chunk instanceof LevelChunk levelChunk) {
-+ levelChunk.setLoaded(false);
-+ PlatformHooks.get().chunkUnloadFromWorld(levelChunk);
-+ }
-+
-+ if (!shouldLevelChunkNotSave) {
-+ this.saveChunk(chunk, true);
-+ } else {
-+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
-+ }
-+
-+ if (chunk instanceof LevelChunk levelChunk) {
-+ this.world.unload(levelChunk);
-+ }
-+ }
-+
-+ // unload entity data
-+ if (entityChunk != null) {
-+ this.saveEntities(entityChunk, true);
-+ // yes this is a hack to pass the compound tag through...
-+ final CompoundTag lastEntityUnload = this.lastEntityUnload;
-+ this.lastEntityUnload = null;
-+
-+ if (entityChunk.unload()) {
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ entityChunk.setTransient(true);
-+ this.entityChunk = entityChunk;
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ } else {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ);
-+ }
-+ // we need to delay the callback until after determining transience, otherwise a potential loader could
-+ // set entityChunk before we do
-+ this.entityDataUnload.completable().complete(lastEntityUnload);
-+ }
-+
-+ // unload poi data
-+ if (poiChunk != null) {
-+ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) {
-+ this.savePOI(poiChunk, true);
-+ } else {
-+ this.poiDataUnload.completable().complete(null);
-+ }
-+
-+ if (poiChunk.isLoaded()) {
-+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
-+ }
-+ }
-+ }
-+
-+ boolean unloadStage3() {
-+ // can only write to these while holding the schedule lock, and we instantly complete them in stage2
-+ this.poiDataUnload = null;
-+ this.entityDataUnload = null;
-+
-+ // we need to check if anything has been loaded in the meantime (or if we have transient entities)
-+ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) {
-+ return false;
-+ }
-+
-+ return this.isSafeToUnload() == null;
-+ }
-+
-+ private void cancelGenTask() {
-+ if (this.generationTask != null) {
-+ this.generationTask.cancel();
-+ } else {
-+ // otherwise, we are blocking on neighbours, so remove them
-+ if (!this.neighboursBlockingGenTask.isEmpty()) {
-+ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) {
-+ if (neighbour.neighboursWaitingForUs.remove(this) == null) {
-+ throw new IllegalStateException("Corrupt state");
-+ }
-+ if (neighbour.neighboursWaitingForUs.isEmpty()) {
-+ neighbour.checkUnload();
-+ }
-+ }
-+ this.neighboursBlockingGenTask.clear();
-+ this.checkUnload();
-+ }
-+ }
-+ }
-+
-+ // holds: ticket level update lock
-+ // holds: schedule lock
-+ public void processTicketLevelUpdate(final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedLoadStatus) {
-+ final int oldLevel = this.oldTicketLevel;
-+ final int newLevel = this.currentTicketLevel;
-+
-+ if (oldLevel == newLevel) {
-+ return;
-+ }
-+
-+ this.oldTicketLevel = newLevel;
-+
-+ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel);
-+ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel);
-+ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
-+ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
-+
-+ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel);
-+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel);
-+
-+ // check for cancellations from downgrading ticket level
-+ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) {
-+ // note: cancel() may invoke onChunkGenComplete synchronously here
-+ if (newUnloaded) {
-+ // need to cancel all tasks
-+ // note: requested status must be set to null here before cancellation, to indicate to the
-+ // completion logic that we do not want rescheduling to occur
-+ this.requestedGenStatus = null;
-+ this.cancelGenTask();
-+ } else {
-+ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus();
-+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus;
-+
-+ if (currentRequestedStatus.isOrAfter(toCancel)) {
-+ // we do have to cancel something here
-+ // clamp requested status to the maximum
-+ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) {
-+ // already generated to status, so we must cancel
-+ this.requestedGenStatus = null;
-+ this.cancelGenTask();
-+ } else {
-+ // not generated to status, so we may have to cancel
-+ // note: gen task is always 1 status above current gen status if not null
-+ this.requestedGenStatus = maxGenerationStatusNew;
-+ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) {
-+ // TOOD is this even possible? i don't think so
-+ throw new IllegalStateException("?????");
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ if (oldState != newState) {
-+ if (newState.isOrAfter(oldState)) {
-+ // status upgrade
-+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) {
-+ // may need to schedule full load
-+ if (this.currentGenStatus != ChunkStatus.FULL) {
-+ if (this.requestedGenStatus != null) {
-+ this.requestedGenStatus = ChunkStatus.FULL;
-+ } else {
-+ this.scheduler.schedule(
-+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks
-+ );
-+ }
-+ }
-+ }
-+ } else {
-+ // status downgrade
-+ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
-+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null);
-+ }
-+
-+ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
-+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null);
-+ }
-+
-+ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) {
-+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null);
-+ }
-+ }
-+
-+ if (this.updatePendingStatus()) {
-+ changedLoadStatus.add(this);
-+ }
-+ }
-+
-+ if (oldUnloaded != newUnloaded) {
-+ this.checkUnload();
-+ }
-+
-+ // Don't really have a choice but to place this hook here
-+ PlatformHooks.get().onChunkHolderTicketChange(this.world, this.vanillaChunkHolder, oldLevel, newLevel);
-+ }
-+
-+ static final int NEIGHBOUR_RADIUS = 2;
-+ private long fullNeighbourChunksLoadedBitset;
-+
-+ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) {
-+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
-+ // optimised variant of the above by moving some of the ops to compile time
-+ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1)));
-+ }
-+ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) {
-+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0;
-+ }
-+
-+ // returns true if this chunk changed pending full status
-+ // must hold scheduling lock
-+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) {
-+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
-+ this.fullNeighbourChunksLoadedBitset |= (1L << index);
-+ return this.updatePendingStatus();
-+ }
-+
-+ // returns true if this chunk changed pending full status
-+ // must hold scheduling lock
-+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) {
-+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
-+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index);
-+ return this.updatePendingStatus();
-+ }
-+
-+ private static long getLoadedMask(final int radius) {
-+ long mask = 0L;
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ mask |= (1L << getFullNeighbourIndex(dx, dz));
-+ }
-+ }
-+
-+ return mask;
-+ }
-+
-+ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0);
-+ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1);
-+ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2);
-+
-+ // only updated while holding scheduling lock
-+ private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE;
-+ // updated while holding no locks, but adds a ticket before to prevent pending status from dropping
-+ // so, current will never update to a value higher than pending
-+ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE;
-+
-+ public FullChunkStatus getChunkStatus() {
-+ // no volatile access, access off-main is considered racey anyways
-+ return this.currentFullChunkStatus;
-+ }
-+
-+ public boolean isEntityTickingReady() {
-+ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING);
-+ }
-+
-+ public boolean isTickingReady() {
-+ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING);
-+ }
-+
-+ public boolean isFullChunkReady() {
-+ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL);
-+ }
-+
-+ private static FullChunkStatus getStatusForBitset(final long bitset) {
-+ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) {
-+ return FullChunkStatus.ENTITY_TICKING;
-+ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) {
-+ return FullChunkStatus.BLOCK_TICKING;
-+ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) {
-+ return FullChunkStatus.FULL;
-+ } else {
-+ return FullChunkStatus.INACCESSIBLE;
-+ }
-+ }
-+
-+ // must hold scheduling lock
-+ // returns whether the pending status was changed
-+ private boolean updatePendingStatus() {
-+ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock
-+
-+ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset);
-+ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
-+ // the bitset is only for chunks that have gone through the status updater
-+ // but here we are ready to go to FULL
-+ pending = FullChunkStatus.FULL;
-+ }
-+
-+ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel
-+ // cannot set above ticket level
-+ pending = byTicketLevel;
-+ }
-+
-+ if (this.pendingFullChunkStatus == pending) {
-+ return false;
-+ }
-+
-+ this.pendingFullChunkStatus = pending;
-+
-+ return true;
-+ }
-+
-+ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) {
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS);
-+ try {
-+ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) {
-+ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) {
-+ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ);
-+ if (loaded) {
-+ if (holder.setNeighbourFullLoaded(-dx, -dz)) {
-+ changedFullStatus.add(holder);
-+ }
-+ } else {
-+ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) {
-+ changedFullStatus.add(holder);
-+ }
-+ }
-+ }
-+ }
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+
-+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus);
-+ }
-+
-+ private boolean processingFullStatus = false;
-+
-+ private void updateCurrentState(final FullChunkStatus to) {
-+ this.currentFullChunkStatus = to;
-+ }
-+
-+ // only to be called on the main thread, no locks need to be held
-+ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
-+
-+ boolean ret = false;
-+
-+ if (this.processingFullStatus) {
-+ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status
-+ return ret;
-+ }
-+
-+ this.processingFullStatus = true;
-+ try {
-+ for (;;) {
-+ // check if we have any remaining work to do
-+
-+ // we do not need to hold the scheduling lock to read pending, as changes to pending
-+ // will queue a status update
-+
-+ final FullChunkStatus pending = this.pendingFullChunkStatus;
-+ FullChunkStatus current = this.currentFullChunkStatus;
-+
-+ if (pending == current) {
-+ if (pending == FullChunkStatus.INACCESSIBLE) {
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ this.checkUnload();
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ }
-+ return ret;
-+ }
-+
-+ ret = true;
-+
-+ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we
-+ // do not need to consider cases where the ticket level may decrease during this call by asynchronous
-+ // ticket changes
-+
-+ // chunks cannot downgrade state while status is pending a change
-+ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE
-+ final LevelChunk chunk = (LevelChunk)this.currentChunk;
-+
-+ // Note: we assume that only load/unload contain plugin logic
-+ // plugin logic is anything stupid enough to possibly change the chunk status while it is already
-+ // being changed (i.e during load it is possible it will try to set to full ticking)
-+ // in order to allow this change, we also need this plugin logic to be contained strictly after all
-+ // of the chunk system load callbacks are invoked
-+ if (pending.isOrAfter(current)) {
-+ // state upgrade
-+ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) {
-+ this.updateCurrentState(FullChunkStatus.FULL);
-+ ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder);
-+ this.scheduler.chunkHolderManager.ensureInAutosave(this);
-+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder);
-+ this.onFullChunkLoadChange(true, changedFullStatus);
-+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk);
-+ }
-+
-+ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
-+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
-+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder);
-+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk);
-+ }
-+
-+ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
-+ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
-+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING);
-+ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder);
-+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk);
-+ }
-+ } else {
-+ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
-+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
-+ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
-+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
-+ }
-+
-+ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
-+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
-+ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder);
-+ this.updateCurrentState(FullChunkStatus.FULL);
-+ }
-+
-+ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) {
-+ this.onFullChunkLoadChange(false, changedFullStatus);
-+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE);
-+ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder);
-+ ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder);
-+ this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
-+ }
-+ }
-+ }
-+ } finally {
-+ this.processingFullStatus = false;
-+ }
-+ }
-+
-+ // note: must hold scheduling lock
-+ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed)
-+ boolean upgradeGenTarget(final ChunkStatus toStatus) {
-+ if (toStatus == null) {
-+ throw new NullPointerException("toStatus cannot be null");
-+ }
-+ if (this.requestedGenStatus == null && this.generationTask == null) {
-+ return false;
-+ }
-+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) {
-+ this.requestedGenStatus = toStatus;
-+ }
-+ return true;
-+ }
-+
-+ public void setGenerationTarget(final ChunkStatus toStatus) {
-+ this.requestedGenStatus = toStatus;
-+ }
-+
-+ public boolean hasGenerationTask() {
-+ return this.generationTask != null;
-+ }
-+
-+ public ChunkStatus getCurrentGenStatus() {
-+ return this.currentGenStatus;
-+ }
-+
-+ public ChunkStatus getRequestedGenStatus() {
-+ return this.requestedGenStatus;
-+ }
-+
-+ private final Reference2ObjectOpenHashMap<ChunkStatus, List<Consumer<ChunkAccess>>> statusWaiters = new Reference2ObjectOpenHashMap<>();
-+
-+ void addStatusConsumer(final ChunkStatus status, final Consumer<ChunkAccess> consumer) {
-+ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> {
-+ return new ArrayList<>(4);
-+ }).add(consumer);
-+ }
-+
-+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
-+ // Update progress listener for LevelLoadingScreen
-+ if (chunk != null) {
-+ final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener;
-+ if (progressListener != null) {
-+ final ChunkStatus finalStatus = status;
-+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus);
-+ });
-+ }
-+ }
-+
-+ // need to tell future statuses to complete if cancelled
-+ do {
-+ this.completeStatusConsumers0(status, chunk);
-+ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus()));
-+ }
-+
-+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) {
-+ final List<Consumer<ChunkAccess>> consumers;
-+ consumers = this.statusWaiters.remove(status);
-+
-+ if (consumers == null) {
-+ return;
-+ }
-+
-+ // must be scheduled to main, we do not trust the callback to not do anything stupid
-+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ for (final Consumer<ChunkAccess> consumer : consumers) {
-+ try {
-+ consumer.accept(chunk);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to process chunk status callback", thr);
-+ }
-+ }
-+ }, Priority.HIGHEST);
-+ }
-+
-+ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>();
-+
-+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer<LevelChunk> consumer) {
-+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> {
-+ return new ArrayList<>(4);
-+ }).add(consumer);
-+ }
-+
-+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) {
-+ final List<Consumer<LevelChunk>> consumers;
-+ consumers = this.fullStatusWaiters.remove(status);
-+
-+ if (consumers == null) {
-+ return;
-+ }
-+
-+ // must be scheduled to main, we do not trust the callback to not do anything stupid
-+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ for (final Consumer<LevelChunk> consumer : consumers) {
-+ try {
-+ consumer.accept(chunk);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to process chunk status callback", thr);
-+ }
-+ }
-+ }, Priority.HIGHEST);
-+ }
-+
-+ // note: must hold scheduling lock
-+ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus,
-+ final List<ChunkProgressionTask> scheduleList, final List<NewChunkHolder> changedLoadStatus) {
-+ if (!this.neighboursBlockingGenTask.isEmpty()) {
-+ throw new IllegalStateException("Cannot have neighbours blocking this gen task");
-+ }
-+ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) {
-+ this.completeStatusConsumers(newStatus, newChunk);
-+ }
-+ // done now, clear state (must be done before scheduling new tasks)
-+ this.generationTask = null;
-+ this.generationTaskStatus = null;
-+ if (newChunk == null) {
-+ // task was cancelled
-+ // should be careful as this could be called while holding the schedule lock and/or inside the
-+ // ticket level update
-+ // while a task may be cancelled, it is possible for it to be later re-scheduled
-+ // however, because generationTask is only set to null on _completion_, the scheduler leaves
-+ // the rescheduling logic to us here
-+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
-+ this.requestedGenStatus = null;
-+ if (requestedGenStatus != null) {
-+ // it looks like it has been requested, so we must reschedule
-+ if (!this.neighboursWaitingForUs.isEmpty()) {
-+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
-+
-+ final NewChunkHolder chunkHolder = entry.getKey();
-+ final ChunkStatus toStatus = entry.getValue();
-+
-+ if (!requestedGenStatus.isOrAfter(toStatus)) {
-+ // if we were cancelled, we are responsible for removing the waiter
-+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
-+ throw new IllegalStateException("Corrupt state");
-+ }
-+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
-+ chunkHolder.checkUnload();
-+ }
-+ iterator.remove();
-+ continue;
-+ }
-+ }
-+ }
-+
-+ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null
-+ this.scheduler.schedule(
-+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
-+ );
-+
-+ // return, can't do anything further
-+ return;
-+ }
-+
-+ if (!this.neighboursWaitingForUs.isEmpty()) {
-+ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) {
-+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
-+ throw new IllegalStateException("Corrupt state");
-+ }
-+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
-+ chunkHolder.checkUnload();
-+ }
-+ }
-+ this.neighboursWaitingForUs.clear();
-+ }
-+ // reset priority, we have nothing left to generate to
-+ this.setPriority(null);
-+ this.checkUnload();
-+ return;
-+ }
-+
-+ this.currentChunk = newChunk;
-+ this.currentGenStatus = newStatus;
-+ final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus);
-+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion);
-+ this.lastChunkCompletion = completion;
-+
-+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
-+
-+ List<NewChunkHolder> needsScheduling = null;
-+ boolean recalculatePriority = false;
-+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator
-+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
-+ final NewChunkHolder neighbour = entry.getKey();
-+ final ChunkStatus requiredStatus = entry.getValue();
-+
-+ if (!newStatus.isOrAfter(requiredStatus)) {
-+ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) {
-+ // if we're cancelled, still need to clear this map
-+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
-+ throw new IllegalStateException("Neighbour is not waiting for us?");
-+ }
-+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
-+ neighbour.checkUnload();
-+ }
-+
-+ iterator.remove();
-+ }
-+ continue;
-+ }
-+
-+ // doesn't matter what isCancelled is here, we need to schedule if we can
-+
-+ recalculatePriority = true;
-+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
-+ throw new IllegalStateException("Neighbour is not waiting for us?");
-+ }
-+
-+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
-+ if (neighbour.requestedGenStatus != null) {
-+ if (needsScheduling == null) {
-+ needsScheduling = new ArrayList<>();
-+ }
-+ needsScheduling.add(neighbour);
-+ } else {
-+ neighbour.checkUnload();
-+ }
-+ }
-+
-+ // remove last; access to entry will throw if removed
-+ iterator.remove();
-+ }
-+
-+ if (newStatus == ChunkStatus.FULL) {
-+ this.lockPriority();
-+ // try to push pending to FULL
-+ if (this.updatePendingStatus()) {
-+ changedLoadStatus.add(this);
-+ }
-+ }
-+
-+ if (recalculatePriority) {
-+ this.recalculateNeighbourRequestedPriority();
-+ }
-+
-+ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) {
-+ this.scheduleNeighbours(needsScheduling, scheduleList);
-+
-+ // we need to schedule more tasks now
-+ this.scheduler.schedule(
-+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
-+ );
-+ } else {
-+ // we're done now
-+ if (requestedGenStatus != null) {
-+ this.requestedGenStatus = null;
-+ }
-+ // reached final stage, so stop scheduling now
-+ this.setPriority(null);
-+ this.checkUnload();
-+
-+ this.scheduleNeighbours(needsScheduling, scheduleList);
-+ }
-+ }
-+
-+ private void scheduleNeighbours(final List<NewChunkHolder> needsScheduling, final List<ChunkProgressionTask> scheduleList) {
-+ if (needsScheduling != null) {
-+ for (int i = 0, len = needsScheduling.size(); i < len; ++i) {
-+ final NewChunkHolder neighbour = needsScheduling.get(i);
-+
-+ this.scheduler.schedule(
-+ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList
-+ );
-+ }
-+ }
-+ }
-+
-+ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus,
-+ final List<NewChunkHolder> neighbours) {
-+ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) {
-+ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!");
-+ }
-+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) {
-+ throw new IllegalStateException("Cannot schedule generation task when not requested");
-+ }
-+ this.generationTask = generationTask;
-+ this.generationTaskStatus = taskStatus;
-+
-+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
-+ neighbours.get(i).addNeighbourUsingChunk();
-+ }
-+
-+ this.checkUnload();
-+
-+ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> {
-+ if (generationTask != this.generationTask) {
-+ throw new IllegalStateException(
-+ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!"
-+ );
-+ }
-+ if (thr != null) {
-+ if (this.genTaskException != null) {
-+ LOGGER.warn("Ignoring exception for " + this.toString(), thr);
-+ return;
-+ }
-+ // don't set generation task to null, so that scheduling will not attempt to create another task and it
-+ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed
-+ // task to complete
-+ this.genTaskException = thr;
-+ this.failedGenStatus = taskStatus;
-+ this.genTaskFailedThread = Thread.currentThread();
-+
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask),
-+ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus)
-+ ), thr);
-+ return;
-+ }
-+
-+ final boolean scheduleTasks;
-+ List<ChunkProgressionTask> tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling();
-+ if (tasks == null) {
-+ scheduleTasks = true;
-+ tasks = new ArrayList<>();
-+ } else {
-+ scheduleTasks = false;
-+ // we are currently updating ticket levels, so we already hold the schedule lock
-+ // this means we have to leave the ticket level update to handle the scheduling
-+ }
-+ final List<NewChunkHolder> changedLoadStatus = new ArrayList<>();
-+ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius.
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius());
-+ try {
-+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
-+ neighbours.get(i).removeNeighbourUsingChunk();
-+ }
-+ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus);
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus);
-+
-+ if (scheduleTasks) {
-+ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after
-+ for (int i = 0, len = tasks.size(); i < len; ++i) {
-+ tasks.get(i).schedule();
-+ }
-+ }
-+ });
-+ }
-+
-+ public PoiChunk getPoiChunk() {
-+ return this.poiChunk;
-+ }
-+
-+ public ChunkEntitySlices getEntityChunk() {
-+ return this.entityChunk;
-+ }
-+
-+ public long lastAutoSave;
-+
-+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
-+
-+ private static final MoonriseRegionFileIO.RegionFileType[] REGION_FILE_TYPES = MoonriseRegionFileIO.RegionFileType.values();
-+
-+ public SaveStat save(final boolean shutdown) {
-+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
-+
-+ ChunkAccess chunk = this.getCurrentChunk();
-+ PoiChunk poi = this.getPoiChunk();
-+ ChunkEntitySlices entities = this.getEntityChunk();
-+ boolean executedUnloadTask = false;
-+ final boolean[] executedUnloadTasks = new boolean[REGION_FILE_TYPES.length];
-+
-+ if (shutdown) {
-+ // make sure that the async unloads complete
-+ if (this.unloadState != null) {
-+ // must have errored during unload
-+ chunk = this.unloadState.chunk();
-+ poi = this.unloadState.poiChunk();
-+ entities = this.unloadState.entityChunk();
-+ }
-+ for (final MoonriseRegionFileIO.RegionFileType regionFileType : REGION_FILE_TYPES) {
-+ final UnloadTask unloadTask = this.getUnloadTask(regionFileType);
-+ if (unloadTask == null) {
-+ continue;
-+ }
-+
-+ final PrioritisedExecutor.PrioritisedTask task = unloadTask.task();
-+ if (task != null && task.isQueued()) {
-+ final boolean executed = task.execute();
-+ executedUnloadTask |= executed;
-+ executedUnloadTasks[regionFileType.ordinal()] = executed;
-+ }
-+ }
-+ }
-+
-+ final boolean forceNoSaveChunk = PlatformHooks.get().forceNoSave(chunk);
-+
-+ // can only synchronously save worldgen chunks during shutdown
-+ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved()));
-+ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty());
-+ boolean canSaveEntities = entities != null;
-+
-+ if (canSaveChunk) {
-+ canSaveChunk = this.saveChunk(chunk, false);
-+ }
-+ if (canSavePOI) {
-+ canSavePOI = this.savePOI(poi, false);
-+ }
-+ if (canSaveEntities) {
-+ // on shutdown, we need to force transient entity chunks to save
-+ canSaveEntities = this.saveEntities(entities, shutdown);
-+ if (shutdown) {
-+ this.lastEntityUnload = null;
-+ }
-+ }
-+
-+ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ?
-+ new SaveStat(
-+ canSaveChunk | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.CHUNK_DATA.ordinal()],
-+ canSaveEntities | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.ENTITY_DATA.ordinal()],
-+ canSavePOI | executedUnloadTasks[MoonriseRegionFileIO.RegionFileType.POI_DATA.ordinal()]
-+ )
-+ : null;
-+ }
-+
-+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) {
-+ if (!chunk.isUnsaved()) {
-+ if (unloading) {
-+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
-+ }
-+ return false;
-+ }
-+ try {
-+ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk);
-+ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData);
-+
-+ chunk.tryMarkSaved();
-+
-+ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>();
-+
-+ final Runnable run = () -> {
-+ final CompoundTag data = chunkData.write();
-+
-+ completable.complete(data);
-+
-+ if (unloading) {
-+ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data);
-+ }
-+ };
-+
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ if (unloading) {
-+ this.chunkDataUnload.toRun().setRunnable(run);
-+ task = this.chunkDataUnload.task();
-+ } else {
-+ task = this.scheduler.saveExecutor.createTask(run);
-+ }
-+
-+ task.queue();
-+
-+ MoonriseRegionFileIO.scheduleSave(
-+ this.world, this.chunkX, this.chunkZ, completable, task, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL
-+ );
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
-+ }
-+
-+ return true;
-+ }
-+
-+ private boolean lastEntitySaveNull;
-+ private CompoundTag lastEntityUnload;
-+ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) {
-+ try {
-+ CompoundTag mergeFrom = null;
-+ if (entities.isTransient()) {
-+ if (!unloading) {
-+ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will
-+ // result in double adding the entities
-+ return false;
-+ }
-+ try {
-+ mergeFrom = MoonriseRegionFileIO.loadData(this.world, this.chunkX, this.chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, Priority.BLOCKING);
-+ } catch (final Exception ex) {
-+ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex);
-+ }
-+ }
-+
-+ final CompoundTag save = entities.save();
-+ if (mergeFrom != null) {
-+ if (save == null) {
-+ // don't override the data on disk with nothing
-+ return false;
-+ } else {
-+ ChunkEntitySlices.copyEntities(mergeFrom, save);
-+ }
-+ }
-+ if (save == null && this.lastEntitySaveNull) {
-+ return false;
-+ }
-+
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA);
-+ this.lastEntitySaveNull = save == null;
-+ if (unloading) {
-+ this.lastEntityUnload = save;
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
-+ }
-+
-+ return true;
-+ }
-+
-+ private boolean lastPoiSaveNull;
-+ private boolean savePOI(final PoiChunk poi, final boolean unloading) {
-+ try {
-+ final CompoundTag save = poi.save();
-+ poi.setDirty(false);
-+ if (save == null && this.lastPoiSaveNull) {
-+ if (unloading) {
-+ this.poiDataUnload.completable().complete(null);
-+ }
-+ return false;
-+ }
-+
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA);
-+ this.lastPoiSaveNull = save == null;
-+ if (unloading) {
-+ this.poiDataUnload.completable().complete(save);
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
-+ }
-+
-+ return true;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
-+ final ChunkEntitySlices entityChunk = this.entityChunk;
-+ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus;
-+ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus;
-+ return "NewChunkHolder{" +
-+ "world=" + WorldUtil.getWorldName(this.world) +
-+ ", chunkX=" + this.chunkX +
-+ ", chunkZ=" + this.chunkZ +
-+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) +
-+ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" +
-+ ", currentGenStatus=" + this.currentGenStatus +
-+ ", requestedGenStatus=" + this.requestedGenStatus +
-+ ", generationTask=" + this.generationTask +
-+ ", generationTaskStatus=" + this.generationTaskStatus +
-+ ", priority=" + this.priority +
-+ ", priorityLocked=" + this.priorityLocked +
-+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority +
-+ ", effective_priority=" + this.getEffectivePriority(null) +
-+ ", oldTicketLevel=" + this.oldTicketLevel +
-+ ", currentTicketLevel=" + this.currentTicketLevel +
-+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk +
-+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset +
-+ ", currentChunkStatus=" + currentFullStatus +
-+ ", pendingChunkStatus=" + pendingFullStatus +
-+ ", is_unload_safe=" + this.isSafeToUnload() +
-+ ", killed=" + this.unloaded +
-+ '}';
-+ }
-+
-+ private static JsonElement serializeStacktraceElement(final StackTraceElement element) {
-+ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
-+ }
-+
-+ private static JsonObject serializeCompletable(final CallbackCompletable<?> completable) {
-+ final JsonObject ret = new JsonObject();
-+
-+ if (completable == null) {
-+ return ret;
-+ }
-+
-+ ret.addProperty("valid", Boolean.TRUE);
-+
-+ final boolean isCompleted = completable.isCompleted();
-+ ret.addProperty("completed", Boolean.valueOf(isCompleted));
-+
-+ if (isCompleted) {
-+ final Throwable throwable = completable.getThrowable();
-+ if (throwable != null) {
-+ final JsonArray throwableJson = new JsonArray();
-+ ret.add("throwable", throwableJson);
-+
-+ for (final StackTraceElement element : throwable.getStackTrace()) {
-+ throwableJson.add(serializeStacktraceElement(element));
-+ }
-+ } else {
-+ final Object result = completable.getResult();
-+ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName()));
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ // (probably) holds ticket and scheduling lock
-+ public JsonObject getDebugJson() {
-+ final JsonObject ret = new JsonObject();
-+
-+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
-+ final ChunkEntitySlices slices = this.entityChunk;
-+ final PoiChunk poiChunk = this.poiChunk;
-+
-+ ret.addProperty("chunkX", Integer.valueOf(this.chunkX));
-+ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ));
-+ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient());
-+ ret.addProperty("poi_chunk", "null=" + (poiChunk == null));
-+ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName());
-+ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString());
-+ ret.addProperty("priority", Objects.toString(this.priority));
-+ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority));
-+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
-+ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload()));
-+ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel));
-+ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel));
-+ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk));
-+
-+ final JsonObject neighbourWaitState = new JsonObject();
-+ ret.add("neighbour_state", neighbourWaitState);
-+
-+ final JsonArray blockingGenNeighbours = new JsonArray();
-+ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours);
-+ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) {
-+ final JsonObject neighbour = new JsonObject();
-+ blockingGenNeighbours.add(neighbour);
-+
-+ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX));
-+ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ));
-+ }
-+
-+ final JsonArray neighboursWaitingForUs = new JsonArray();
-+ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs);
-+ for (final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) {
-+ final NewChunkHolder holder = entry.getKey();
-+ final ChunkStatus status = entry.getValue();
-+
-+ final JsonObject neighbour = new JsonObject();
-+ neighboursWaitingForUs.add(neighbour);
-+
-+
-+ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX));
-+ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ));
-+ neighbour.addProperty("waiting_for", Objects.toString(status));
-+ }
-+
-+ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus));
-+ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus));
-+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
-+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus));
-+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null));
-+ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null));
-+
-+ final UnloadTask entityDataUnload = this.entityDataUnload;
-+ final UnloadTask poiDataUnload = this.poiDataUnload;
-+ final UnloadTask chunkDataUnload = this.chunkDataUnload;
-+
-+ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable()));
-+ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable()));
-+ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable()));
-+
-+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
-+ if (unloadTask == null) {
-+ ret.addProperty("unload_task_priority", "null");
-+ ret.addProperty("unload_task_suborder", Long.valueOf(0L));
-+ } else {
-+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority()));
-+ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder()));
-+ }
-+
-+ ret.addProperty("killed", Boolean.valueOf(this.unloaded));
-+
-+ return ret;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-@@ -0,0 +1,215 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import java.lang.invoke.VarHandle;
-+
-+public abstract class PriorityHolder {
-+
-+ protected volatile int priority;
-+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class);
-+
-+ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0;
-+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1;
-+
-+ protected final int getPriorityVolatile() {
-+ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this);
-+ }
-+
-+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
-+ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update);
-+ }
-+
-+ protected final int getAndOrPriorityVolatile(final int val) {
-+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val);
-+ }
-+
-+ protected final void setPriorityPlain(final int val) {
-+ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val);
-+ }
-+
-+ protected PriorityHolder(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.setPriorityPlain(priority.priority);
-+ }
-+
-+ // used only for debug json
-+ public boolean isScheduled() {
-+ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0;
-+ }
-+
-+ // returns false if cancelled
-+ public boolean markExecuting() {
-+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
-+ }
-+
-+ public boolean isMarkedExecuted() {
-+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
-+ }
-+
-+ public void cancel() {
-+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
-+ // cancelled already
-+ return;
-+ }
-+ this.cancelScheduled();
-+ }
-+
-+ public void schedule() {
-+ int priority = this.getPriorityVolatile();
-+
-+ if ((priority & PRIORITY_SCHEDULED) != 0) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled
-+ return;
-+ }
-+
-+ this.scheduleTask(Priority.getPriority(priority));
-+
-+ int failures = 0;
-+ for (;;) {
-+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) {
-+ return;
-+ }
-+
-+ if ((priority & PRIORITY_SCHEDULED) != 0) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ this.setPriorityScheduled(Priority.getPriority(priority));
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public final Priority getPriority() {
-+ final int ret = this.getPriorityVolatile();
-+ if ((ret & PRIORITY_EXECUTED) != 0) {
-+ return Priority.COMPLETING;
-+ }
-+ if ((ret & PRIORITY_SCHEDULED) != 0) {
-+ return this.getScheduledPriority();
-+ }
-+ return Priority.getPriority(ret);
-+ }
-+
-+ public final void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_SCHEDULED) != 0) {
-+ this.lowerPriorityScheduled(priority);
-+ return;
-+ }
-+
-+ if (!priority.isLowerPriority(curr)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public final void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_SCHEDULED) != 0) {
-+ this.setPriorityScheduled(priority);
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public final void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_SCHEDULED) != 0) {
-+ this.raisePriorityScheduled(priority);
-+ return;
-+ }
-+
-+ if (!priority.isHigherPriority(curr)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ protected abstract void cancelScheduled();
-+
-+ protected abstract Priority getScheduledPriority();
-+
-+ protected abstract void scheduleTask(final Priority priority);
-+
-+ protected abstract void lowerPriorityScheduled(final Priority priority);
-+
-+ protected abstract void setPriorityScheduled(final Priority priority);
-+
-+ protected abstract void raisePriorityScheduled(final Priority priority);
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
-@@ -0,0 +1,1457 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
-+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.Short2ByteMap;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.concurrent.locks.LockSupport;
-+
-+public abstract class ThreadedTicketLevelPropagator {
-+
-+ // sections are 64 in length
-+ public static final int SECTION_SHIFT = 6;
-+ public static final int SECTION_SIZE = 1 << SECTION_SHIFT;
-+ private static final int LEVEL_BITS = SECTION_SHIFT;
-+ private static final int LEVEL_COUNT = 1 << LEVEL_BITS;
-+ private static final int MIN_SOURCE_LEVEL = 1;
-+ // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate
-+ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation
-+ private static final int MAX_SOURCE_LEVEL = 62;
-+
-+ private static int getMaxSchedulingRadius() {
-+ return 2 * ChunkTaskScheduler.getMaxAccessRadius();
-+ }
-+
-+ private final UpdateQueue updateQueue;
-+ private final ConcurrentLong2ReferenceChainedHashTable<Section> sections;
-+
-+ public ThreadedTicketLevelPropagator() {
-+ this.updateQueue = new UpdateQueue();
-+ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>();
-+ }
-+
-+ // must hold ticket lock for:
-+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
-+ public void setSource(final int posX, final int posZ, final int to) {
-+ if (to < 1 || to > MAX_SOURCE_LEVEL) {
-+ throw new IllegalArgumentException("Source: " + to);
-+ }
-+
-+ final int sectionX = posX >> SECTION_SHIFT;
-+ final int sectionZ = posZ >> SECTION_SHIFT;
-+
-+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ Section section = this.sections.get(coordinate);
-+ if (section == null) {
-+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) {
-+ throw new IllegalStateException("Race condition while creating new section");
-+ }
-+ }
-+
-+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
-+ final short sLocalIdx = (short)localIdx;
-+
-+ final short sourceAndLevel = section.levels[localIdx];
-+ final int currentSource = (sourceAndLevel >>> 8) & 0xFF;
-+
-+ if (currentSource == to) {
-+ // nothing to do
-+ // make sure to kill the current update, if any
-+ section.queuedSources.replace(sLocalIdx, (byte)to);
-+ return;
-+ }
-+
-+ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
-+ this.queueSectionUpdate(section);
-+ }
-+ }
-+
-+ // must hold ticket lock for:
-+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
-+ public void removeSource(final int posX, final int posZ) {
-+ final int sectionX = posX >> SECTION_SHIFT;
-+ final int sectionZ = posZ >> SECTION_SHIFT;
-+
-+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final Section section = this.sections.get(coordinate);
-+
-+ if (section == null) {
-+ return;
-+ }
-+
-+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
-+ final short sLocalIdx = (short)localIdx;
-+
-+ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF;
-+
-+ if (currentSource == 0) {
-+ // we use replace here so that we do not possibly multi-queue a section for an update
-+ section.queuedSources.replace(sLocalIdx, (byte)0);
-+ return;
-+ }
-+
-+ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
-+ this.queueSectionUpdate(section);
-+ }
-+ }
-+
-+ private void queueSectionUpdate(final Section section) {
-+ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null));
-+ }
-+
-+ public boolean hasPendingUpdates() {
-+ return !this.updateQueue.isEmpty();
-+ }
-+
-+ // holds ticket lock for every chunk section represented by any position in the key set
-+ // updates is modifiable and passed to processSchedulingUpdates after this call
-+ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates);
-+
-+ // holds ticket lock for every chunk section represented by any position in the key set
-+ // holds scheduling lock in max access radius for every position held by the ticket lock
-+ // updates is cleared after this call
-+ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
-+ final List<NewChunkHolder> changedFullStatus);
-+
-+ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ
-+ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock,
-+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
-+ if (!this.hasPendingUpdates()) {
-+ return false;
-+ }
-+
-+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final Section section = this.sections.get(coordinate);
-+
-+ if (section == null || section.queuedSources.isEmpty()) {
-+ // no section or no updates
-+ return false;
-+ }
-+
-+ final Propagator propagator = Propagator.acquirePropagator();
-+ final boolean ret = this.performUpdate(section, null, propagator,
-+ null, schedulingLock, scheduledTasks, changedFullStatus
-+ );
-+ Propagator.returnPropagator(propagator);
-+ return ret;
-+ }
-+
-+ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator,
-+ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
-+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
-+ final int sectionX = section.sectionX;
-+ final int sectionZ = section.sectionZ;
-+
-+ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT;
-+ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT;
-+ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
-+ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
-+
-+ // set up encode offset first as we need to queue level changes _before_
-+ propagator.setupEncodeOffset(sectionX, sectionZ);
-+
-+ final int coordinateOffset = propagator.coordinateOffset;
-+
-+ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ);
-+ final boolean ret;
-+ try {
-+ // first, check if this update was stolen
-+ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) {
-+ // occurs when a stolen update deletes this section
-+ // it is possible that another update is scheduled, but that one will have the correct section
-+ if (node != null) {
-+ this.updateQueue.remove(node);
-+ }
-+ return false;
-+ }
-+
-+ final int oldSourceSize = section.sources.size();
-+
-+ // process pending sources
-+ for (final Iterator<Short2ByteMap.Entry> iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) {
-+ final Short2ByteMap.Entry entry = iterator.next();
-+ final int pos = (int)entry.getShortKey();
-+ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT);
-+ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT);
-+ final int newSource = (int)entry.getByteValue();
-+
-+ final short currentEncoded = section.levels[pos];
-+ final int currLevel = currentEncoded & 0xFF;
-+ final int prevSource = (currentEncoded >>> 8) & 0xFF;
-+
-+ if (prevSource == newSource) {
-+ // nothing changed
-+ continue;
-+ }
-+
-+ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) {
-+ // just update the source, don't need to propagate change
-+ section.levels[pos] = (short)(currLevel | (newSource << 8));
-+ // level is unchanged, don't add to changed positions
-+ } else {
-+ // set current level and current source to new source
-+ section.levels[pos] = (short)(newSource | (newSource << 8));
-+ // must add to updated positions in case this is final
-+ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource);
-+ if (newSource != 0) {
-+ // queue increase with new source level
-+ propagator.appendToIncreaseQueue(
-+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
-+ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
-+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
-+ );
-+ }
-+ // queue decrease with previous level
-+ if (newSource < currLevel) {
-+ propagator.appendToDecreaseQueue(
-+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
-+ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
-+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
-+ );
-+ }
-+ }
-+
-+ if (newSource == 0) {
-+ // prevSource != newSource, so we are removing this source
-+ section.sources.remove((short)pos);
-+ } else if (prevSource == 0) {
-+ // prevSource != newSource, so we are adding this source
-+ section.sources.add((short)pos);
-+ }
-+ }
-+
-+ section.queuedSources.clear();
-+
-+ final int newSourceSize = section.sources.size();
-+
-+ if (oldSourceSize == 0 && newSourceSize != 0) {
-+ // need to make sure the sections in 1 radius are initialised
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+ final int offX = dx + sectionX;
-+ final int offZ = dz + sectionZ;
-+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
-+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> {
-+ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap));
-+ });
-+
-+ // increase ref count
-+ ++neighbour.oneRadNeighboursWithSources;
-+ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) {
-+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
-+ }
-+ }
-+ }
-+ }
-+
-+ if (propagator.hasUpdates()) {
-+ propagator.setupCaches(this, sectionX, sectionZ, 1);
-+ propagator.performDecrease();
-+ // don't need try-finally, as any exception will cause the propagator to not be returned
-+ propagator.destroyCaches();
-+ }
-+
-+ if (newSourceSize == 0) {
-+ final boolean decrementRef = oldSourceSize != 0;
-+ // check for section de-init
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final int offX = dx + sectionX;
-+ final int offZ = dz + sectionZ;
-+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
-+ final Section neighbour = this.sections.get(coordinate);
-+
-+ if (neighbour == null) {
-+ if (oldSourceSize == 0 && (dx | dz) != 0) {
-+ // since we don't have sources, this section is allowed to be null
-+ continue;
-+ }
-+ throw new IllegalStateException("??");
-+ }
-+
-+ if (decrementRef && (dx | dz) != 0) {
-+ // decrease ref count, but only for neighbours
-+ --neighbour.oneRadNeighboursWithSources;
-+ }
-+
-+ // we need to check the current section for de-init as well
-+ if (neighbour.oneRadNeighboursWithSources == 0) {
-+ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) {
-+ // need to de-init
-+ this.sections.remove(coordinate);
-+ } // else: neighbour is queued for an update, and it will de-init itself
-+ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) {
-+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
-+ }
-+ }
-+ }
-+ }
-+
-+
-+ ret = !propagator.updatedPositions.isEmpty();
-+
-+ if (ret) {
-+ this.processLevelUpdates(propagator.updatedPositions);
-+
-+ if (!propagator.updatedPositions.isEmpty()) {
-+ // now we can actually update the ticket levels in the chunk holders
-+ final int maxScheduleRadius = getMaxSchedulingRadius();
-+
-+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time
-+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock(
-+ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius,
-+ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius
-+ );
-+ try {
-+ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus);
-+ } finally {
-+ schedulingLock.unlock(schedulingNode);
-+ }
-+ }
-+
-+ propagator.updatedPositions.clear();
-+ }
-+ } finally {
-+ if (ticketLock != null) {
-+ ticketLock.unlock(ticketNode);
-+ }
-+ }
-+
-+ // finished
-+ if (node != null) {
-+ this.updateQueue.remove(node);
-+ }
-+
-+ return ret;
-+ }
-+
-+ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
-+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
-+ if (this.updateQueue.isEmpty()) {
-+ return false;
-+ }
-+
-+ final long maxOrder = this.updateQueue.getLastOrder();
-+
-+ boolean updated = false;
-+ Propagator propagator = null;
-+
-+ for (;;) {
-+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder);
-+ if (toUpdate == null) {
-+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) {
-+ if (propagator != null) {
-+ Propagator.returnPropagator(propagator);
-+ }
-+ return updated;
-+ }
-+
-+ continue;
-+ }
-+
-+ if (propagator == null) {
-+ propagator = Propagator.acquirePropagator();
-+ }
-+
-+ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus);
-+ }
-+ }
-+
-+ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer
-+ // for the last update node being handled
-+ private static final class UpdateQueue {
-+
-+ private volatile UpdateQueueNode head;
-+ private volatile UpdateQueueNode tail;
-+
-+ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class);
-+ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class);
-+
-+ /* head */
-+
-+ private final void setHeadPlain(final UpdateQueueNode newHead) {
-+ HEAD_HANDLE.set(this, newHead);
-+ }
-+
-+ private final void setHeadOpaque(final UpdateQueueNode newHead) {
-+ HEAD_HANDLE.setOpaque(this, newHead);
-+ }
-+
-+ private final UpdateQueueNode getHeadPlain() {
-+ return (UpdateQueueNode)HEAD_HANDLE.get(this);
-+ }
-+
-+ private final UpdateQueueNode getHeadOpaque() {
-+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this);
-+ }
-+
-+ private final UpdateQueueNode getHeadAcquire() {
-+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this);
-+ }
-+
-+ /* tail */
-+
-+ private final void setTailPlain(final UpdateQueueNode newTail) {
-+ TAIL_HANDLE.set(this, newTail);
-+ }
-+
-+ private final void setTailOpaque(final UpdateQueueNode newTail) {
-+ TAIL_HANDLE.setOpaque(this, newTail);
-+ }
-+
-+ private final UpdateQueueNode getTailPlain() {
-+ return (UpdateQueueNode)TAIL_HANDLE.get(this);
-+ }
-+
-+ private final UpdateQueueNode getTailOpaque() {
-+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this);
-+ }
-+
-+ public UpdateQueue() {
-+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null);
-+ dummy.order = -1L;
-+ dummy.preventAdds();
-+
-+ this.setHeadPlain(dummy);
-+ this.setTailPlain(dummy);
-+ }
-+
-+ public boolean isEmpty() {
-+ return this.peek() == null;
-+ }
-+
-+ public boolean hasRemainingUpdates(final long maxUpdate) {
-+ final UpdateQueueNode node = this.peek();
-+ return node != null && node.order <= maxUpdate;
-+ }
-+
-+ public long getLastOrder() {
-+ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) {
-+ final UpdateQueueNode next = curr.getNextVolatile();
-+ if (next == null) {
-+ // try to update stale tail
-+ if (this.getTailOpaque() == tail && curr != tail) {
-+ this.setTailOpaque(curr);
-+ }
-+ return curr.order;
-+ }
-+ curr = next;
-+ }
-+ }
-+
-+ private static void await(final UpdateQueueNode node) {
-+ final Thread currThread = Thread.currentThread();
-+ // we do not use add-blocking because we use the nullability of the section to block
-+ // remove() does not begin to poll from the wait queue until the section is null'd,
-+ // and so provided we check the nullability before parking there is no ordering of these operations
-+ // such that remove() finishes polling from the wait queue while section is not null
-+ node.add(currThread);
-+
-+ // wait until completed
-+ while (node.getSectionVolatile() != null) {
-+ LockSupport.park();
-+ }
-+ }
-+
-+ public UpdateQueueNode acquireNextOrWait(final long maxOrder) {
-+ final List<UpdateQueueNode> blocking = new ArrayList<>();
-+
-+ node_search:
-+ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) {
-+ if (curr.getSectionVolatile() == null) {
-+ continue;
-+ }
-+
-+ if (curr.getUpdatingVolatile()) {
-+ blocking.add(curr);
-+ continue;
-+ }
-+
-+ for (int i = 0, len = blocking.size(); i < len; ++i) {
-+ final UpdateQueueNode node = blocking.get(i);
-+
-+ if (node.intersects(curr)) {
-+ continue node_search;
-+ }
-+ }
-+
-+ if (curr.getAndSetUpdatingVolatile(true)) {
-+ blocking.add(curr);
-+ continue;
-+ }
-+
-+ return curr;
-+ }
-+
-+ if (!blocking.isEmpty()) {
-+ await(blocking.get(0));
-+ }
-+
-+ return null;
-+ }
-+
-+ public UpdateQueueNode peek() {
-+ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) {
-+ final UpdateQueueNode next = curr.getNextVolatile();
-+ final Section element = curr.getSectionVolatile(); /* Likely in sync */
-+
-+ if (element != null) {
-+ if (this.getHeadOpaque() == head && curr != head) {
-+ this.setHeadOpaque(curr);
-+ }
-+ return curr;
-+ }
-+
-+ if (next == null) {
-+ if (this.getHeadOpaque() == head && curr != head) {
-+ this.setHeadOpaque(curr);
-+ }
-+ return null;
-+ }
-+ curr = next;
-+ }
-+ }
-+
-+ public void remove(final UpdateQueueNode node) {
-+ // mark as removed
-+ node.setSectionVolatile(null);
-+
-+ // use peek to advance head
-+ this.peek();
-+
-+ // unpark any waiters / block the wait queue
-+ Thread unpark;
-+ while ((unpark = node.poll()) != null) {
-+ LockSupport.unpark(unpark);
-+ }
-+ }
-+
-+ public void append(final UpdateQueueNode node) {
-+ int failures = 0;
-+
-+ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) {
-+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
-+ /* It is likely due to a cache miss caused by another write to the next field */
-+ final UpdateQueueNode next = curr.getNextVolatile();
-+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (next == null) {
-+ node.order = curr.order + 1L;
-+ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node);
-+
-+ if (compared == null) {
-+ /* Added */
-+ /* Avoid CASing on tail more than we need to */
-+ /* CAS to avoid setting an out-of-date tail */
-+ if (this.getTailOpaque() == currTail) {
-+ this.setTailOpaque(node);
-+ }
-+ return;
-+ }
-+
-+ ++failures;
-+ curr = compared;
-+ continue;
-+ }
-+
-+ if (curr == currTail) {
-+ /* Tail is likely not up-to-date */
-+ curr = next;
-+ } else {
-+ /* Try to update to tail */
-+ if (currTail == (currTail = this.getTailOpaque())) {
-+ curr = next;
-+ } else {
-+ curr = currTail;
-+ }
-+ }
-+ }
-+ }
-+
-+ // each node also represents a set of waiters, represented by the MTQ
-+ // if the queue is add-blocked, then the update is complete
-+ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> {
-+ private final int sectionX;
-+ private final int sectionZ;
-+
-+ private long order;
-+ private volatile Section section;
-+ private volatile UpdateQueueNode next;
-+ private volatile boolean updating;
-+
-+ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class);
-+ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class);
-+ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class);
-+
-+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) {
-+ if (section == null) {
-+ this.sectionX = this.sectionZ = 0;
-+ } else {
-+ this.sectionX = section.sectionX;
-+ this.sectionZ = section.sectionZ;
-+ }
-+
-+ SECTION_HANDLE.set(this, section);
-+ NEXT_HANDLE.set(this, next);
-+ }
-+
-+ public boolean intersects(final UpdateQueueNode other) {
-+ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ));
-+
-+ // intersection radius is ticket update radius (1) + scheduling radius
-+ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT));
-+ }
-+
-+ /* section */
-+
-+ private final Section getSectionPlain() {
-+ return (Section)SECTION_HANDLE.get(this);
-+ }
-+
-+ private final Section getSectionVolatile() {
-+ return (Section)SECTION_HANDLE.getVolatile(this);
-+ }
-+
-+ private final void setSectionPlain(final Section update) {
-+ SECTION_HANDLE.set(this, update);
-+ }
-+
-+ private final void setSectionOpaque(final Section update) {
-+ SECTION_HANDLE.setOpaque(this, update);
-+ }
-+
-+ private final void setSectionVolatile(final Section update) {
-+ SECTION_HANDLE.setVolatile(this, update);
-+ }
-+
-+ private final Section getAndSetSectionVolatile(final Section update) {
-+ return (Section)SECTION_HANDLE.getAndSet(this, update);
-+ }
-+
-+ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) {
-+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update);
-+ }
-+
-+ /* next */
-+
-+ private final UpdateQueueNode getNextPlain() {
-+ return (UpdateQueueNode)NEXT_HANDLE.get(this);
-+ }
-+
-+ private final UpdateQueueNode getNextOpaque() {
-+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this);
-+ }
-+
-+ private final UpdateQueueNode getNextAcquire() {
-+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this);
-+ }
-+
-+ private final UpdateQueueNode getNextVolatile() {
-+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this);
-+ }
-+
-+ private final void setNextPlain(final UpdateQueueNode next) {
-+ NEXT_HANDLE.set(this, next);
-+ }
-+
-+ private final void setNextVolatile(final UpdateQueueNode next) {
-+ NEXT_HANDLE.setVolatile(this, next);
-+ }
-+
-+ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) {
-+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set);
-+ }
-+
-+ /* updating */
-+
-+ private final boolean getUpdatingVolatile() {
-+ return (boolean)UPDATING_HANDLE.getVolatile(this);
-+ }
-+
-+ private final boolean getAndSetUpdatingVolatile(final boolean value) {
-+ return (boolean)UPDATING_HANDLE.getAndSet(this, value);
-+ }
-+ }
-+ }
-+
-+ private static final class Section {
-+
-+ // upper 8 bits: sources, lower 8 bits: level
-+ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle
-+ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back.
-+ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE];
-+ // set of local positions that represent sources
-+ private final ShortOpenHashSet sources = new ShortOpenHashSet();
-+ // map of local index to new source level
-+ // the source level _cannot_ be updated in the backing storage immediately since the update
-+ private static final byte NO_QUEUED_UPDATE = (byte)-1;
-+ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap();
-+ {
-+ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE);
-+ }
-+ private int oneRadNeighboursWithSources = 0;
-+
-+ public final int sectionX;
-+ public final int sectionZ;
-+
-+ public Section(final int sectionX, final int sectionZ) {
-+ this.sectionX = sectionX;
-+ this.sectionZ = sectionZ;
-+ }
-+
-+ public boolean isZero() {
-+ for (final short val : this.levels) {
-+ if (val != 0) {
-+ return false;
-+ }
-+ }
-+ return true;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ final StringBuilder ret = new StringBuilder();
-+
-+ for (int x = 0; x < SECTION_SIZE; ++x) {
-+ ret.append("levels x=").append(x).append("\n");
-+ for (int z = 0; z < SECTION_SIZE; ++z) {
-+ final short v = this.levels[x | (z << SECTION_SHIFT)];
-+ ret.append(v & 0xFF).append(".");
-+ }
-+ ret.append("\n");
-+ ret.append("sources x=").append(x).append("\n");
-+ for (int z = 0; z < SECTION_SIZE; ++z) {
-+ final short v = this.levels[x | (z << SECTION_SHIFT)];
-+ ret.append((v >>> 8) & 0xFF).append(".");
-+ }
-+ ret.append("\n\n");
-+ }
-+
-+ return ret.toString();
-+ }
-+ }
-+
-+
-+ private static final class Propagator {
-+
-+ private static final ArrayDeque<Propagator> CACHED_PROPAGATORS = new ArrayDeque<>();
-+ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2;
-+
-+ private static Propagator acquirePropagator() {
-+ synchronized (CACHED_PROPAGATORS) {
-+ final Propagator ret = CACHED_PROPAGATORS.pollFirst();
-+ if (ret != null) {
-+ return ret;
-+ }
-+ }
-+ return new Propagator();
-+ }
-+
-+ private static void returnPropagator(final Propagator propagator) {
-+ synchronized (CACHED_PROPAGATORS) {
-+ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) {
-+ CACHED_PROPAGATORS.add(propagator);
-+ }
-+ }
-+ }
-+
-+ private static final int SECTION_RADIUS = 2;
-+ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1;
-+ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
-+ private static final int COORDINATE_BITS = 9;
-+ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS;
-+ static {
-+ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) {
-+ throw new IllegalStateException("Adjust COORDINATE_BITS");
-+ }
-+ }
-+ // index = x + (z * SECTION_CACHE_WIDTH)
-+ // (this requires x >= 0 and z >= 0)
-+ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH];
-+
-+ private int encodeOffsetX;
-+ private int encodeOffsetZ;
-+
-+ private int coordinateOffset;
-+
-+ private int encodeSectionOffsetX;
-+ private int encodeSectionOffsetZ;
-+
-+ private int sectionIndexOffset;
-+
-+ public final boolean hasUpdates() {
-+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0;
-+ }
-+
-+ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) {
-+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1);
-+ // must have that encoded >= 0
-+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE]
-+ // we want a range of [0, maxCoordinate*2]
-+ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset
-+ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT);
-+ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT);
-+
-+ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
-+ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS)
-+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS);
-+
-+ // need encoded values to be >= 0
-+ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset
-+ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX;
-+ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ;
-+
-+ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH)
-+ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH);
-+ }
-+
-+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad
-+ // must call setupEncodeOffset
-+ private final void setupCaches(final ThreadedTicketLevelPropagator propagator,
-+ final int centerSectionX, final int centerSectionZ,
-+ final int rad) {
-+ for (int dz = -rad; dz <= rad; ++dz) {
-+ for (int dx = -rad; dx <= rad; ++dx) {
-+ final int sectionX = centerSectionX + dx;
-+ final int sectionZ = centerSectionZ + dz;
-+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final Section section = propagator.sections.get(coordinate);
-+
-+ if (section == null) {
-+ throw new IllegalStateException("Section at " + coordinate + " should not be null");
-+ }
-+
-+ this.setSectionInCache(sectionX, sectionZ, section);
-+ }
-+ }
-+ }
-+
-+ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) {
-+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section;
-+ }
-+
-+ private final Section getSection(final int sectionX, final int sectionZ) {
-+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset];
-+ }
-+
-+ private final int getLevel(final int posX, final int posZ) {
-+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
-+ if (section != null) {
-+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF;
-+ }
-+
-+ return 0;
-+ }
-+
-+ private final void setLevel(final int posX, final int posZ, final int to) {
-+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
-+ if (section != null) {
-+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
-+ final short level = section.levels[index];
-+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF));
-+ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to);
-+ }
-+ }
-+
-+ private final void destroyCaches() {
-+ Arrays.fill(this.sections, null);
-+ }
-+
-+ // contains:
-+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS))
-+ // next LEVEL_BITS (6) bits: propagated level [0, 63]
-+ // propagation directions bitset (16 bits):
-+ private static final long ALL_DIRECTIONS_BITSET = (
-+ // z = -1
-+ (1L << ((1 - 1) | ((1 - 1) << 2))) |
-+ (1L << ((1 + 0) | ((1 - 1) << 2))) |
-+ (1L << ((1 + 1) | ((1 - 1) << 2))) |
-+
-+ // z = 0
-+ (1L << ((1 - 1) | ((1 + 0) << 2))) |
-+ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0)
-+ (1L << ((1 + 1) | ((1 + 0) << 2))) |
-+
-+ // z = 1
-+ (1L << ((1 - 1) | ((1 + 1) << 2))) |
-+ (1L << ((1 + 0) | ((1 + 1) << 2))) |
-+ (1L << ((1 + 1) | ((1 + 1) << 2)))
-+ );
-+
-+ private void ex(int bitset) {
-+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
-+ final int set = Integer.numberOfTrailingZeros(bitset);
-+ final int tailingBit = (-bitset) & bitset;
-+ // XOR to remove the trailing bit
-+ bitset ^= tailingBit;
-+
-+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
-+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
-+ // index of the set bit is the encoded value
-+ // the encoded coordinate has 3 valid states:
-+ // 0b00 (0) -> -1
-+ // 0b01 (1) -> 0
-+ // 0b10 (2) -> 1
-+ // the decode operation then is val - 1, and the encode operation is val + 1
-+ final int xOff = (set & 3) - 1;
-+ final int zOff = ((set >>> 2) & 3) - 1;
-+ System.out.println("Encoded: (" + xOff + "," + zOff + ")");
-+ }
-+ }
-+
-+ private void ch(long bs, int shift) {
-+ int bitset = (int)(bs >>> shift);
-+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
-+ final int set = Integer.numberOfTrailingZeros(bitset);
-+ final int tailingBit = (-bitset) & bitset;
-+ // XOR to remove the trailing bit
-+ bitset ^= tailingBit;
-+
-+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
-+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
-+ // index of the set bit is the encoded value
-+ // the encoded coordinate has 3 valid states:
-+ // 0b00 (0) -> -1
-+ // 0b01 (1) -> 0
-+ // 0b10 (2) -> 1
-+ // the decode operation then is val - 1, and the encode operation is val + 1
-+ final int xOff = (set & 3) - 1;
-+ final int zOff = ((set >>> 2) & 3) - 1;
-+ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) {
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading
-+ // updates for sources
-+ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1;
-+ // whether the propagation needs to check if its current level is equal to the expected level
-+ // used only in increase propagation
-+ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0;
-+
-+ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
-+ private int increaseQueueInitialLength;
-+ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
-+ private int decreaseQueueInitialLength;
-+
-+ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap();
-+
-+ private final long[] resizeIncreaseQueue() {
-+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
-+ }
-+
-+ private final long[] resizeDecreaseQueue() {
-+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
-+ }
-+
-+ private final void appendToIncreaseQueue(final long value) {
-+ final int idx = this.increaseQueueInitialLength++;
-+ long[] queue = this.increaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ queue[idx] = value;
-+ return;
-+ } else {
-+ queue[idx] = value;
-+ return;
-+ }
-+ }
-+
-+ private final void appendToDecreaseQueue(final long value) {
-+ final int idx = this.decreaseQueueInitialLength++;
-+ long[] queue = this.decreaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ queue[idx] = value;
-+ return;
-+ } else {
-+ queue[idx] = value;
-+ return;
-+ }
-+ }
-+
-+ private final void performIncrease() {
-+ long[] queue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.increaseQueueInitialLength;
-+ this.increaseQueueInitialLength = 0;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.sectionIndexOffset;
-+
-+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
-+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
-+ // note: the above code requires coordinate bits * 2 < 32
-+ // bitset is 16 bits
-+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
-+
-+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
-+ if (this.getLevel(posX, posZ) != propagatedLevel) {
-+ // not at the level we expect, so something changed.
-+ continue;
-+ }
-+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
-+ // these are used to restore sources after a propagation decrease
-+ this.setLevel(posX, posZ, propagatedLevel);
-+ }
-+
-+ // this bitset represents the values that we have not propagated to
-+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
-+ // significantly reducing the total number of ops
-+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
-+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
-+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
-+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
-+ // index = x | (z << 3)
-+
-+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
-+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
-+ // but the rest not propagated are already handled
-+ long currentPropagation = ~(
-+ // z = -1
-+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
-+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
-+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
-+
-+ // z = 0
-+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
-+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
-+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
-+
-+ // z = 1
-+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
-+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
-+ (1L << ((2 + 1) | ((2 + 1) << 3)))
-+ );
-+
-+ final int toPropagate = propagatedLevel - 1;
-+
-+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
-+ // the bits, the cpu loop predictor should perfectly predict the loop.
-+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
-+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
-+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
-+ propagateDirectionBitset ^= tailingBit;
-+
-+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
-+ // it has been split to save some cycles via parallelism
-+ final int pDecodeX = (set & 3);
-+ final int pDecodeZ = ((set >>> 2) & 3);
-+
-+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
-+ final int offX = (posX - 1) + pDecodeX;
-+ final int offZ = (posZ - 1) + pDecodeZ;
-+
-+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
-+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
-+
-+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
-+ // bitset idx = x | (z << 3)
-+
-+ // read three bits, so we need 7L
-+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
-+ // nstartidx1 = x rel -1 for z rel -1
-+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
-+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
-+ // = pDecodeX | (pDecodeZ << 3) = start
-+ final int start = pDecodeX | (pDecodeZ << 3);
-+ final long bitsetLine1 = currentPropagation & (7L << (start));
-+
-+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
-+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
-+
-+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
-+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
-+
-+ // remove ("take") lines from bitset
-+ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
-+
-+ // now try to propagate
-+ final Section section = this.sections[sectionIndex];
-+
-+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
-+ final short currentStoredLevel = section.levels[localIndex];
-+ final int currentLevel = currentStoredLevel & 0xFF;
-+
-+ if (currentLevel >= toPropagate) {
-+ continue; // already at the level we want
-+ }
-+
-+ // update level
-+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF));
-+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate);
-+
-+ // queue next
-+ if (toPropagate > 1) {
-+ // now combine into one bitset to pass to child
-+ // the child bitset is 4x4, so we just shift each line by 4
-+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
-+ final long childPropagation =
-+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
-+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
-+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
-+
-+ // don't queue update if toPropagate cannot propagate anything to neighbours
-+ // (for increase, propagating 0 to neighbours is useless)
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
-+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
-+ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS));
-+ continue;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+
-+ private final void performDecrease() {
-+ long[] queue = this.decreaseQueue;
-+ long[] increaseQueue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.decreaseQueueInitialLength;
-+ this.decreaseQueueInitialLength = 0;
-+ int increaseQueueLength = this.increaseQueueInitialLength;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.sectionIndexOffset;
-+
-+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
-+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
-+ // note: the above code requires coordinate bits * 2 < 32
-+ // bitset is 16 bits
-+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
-+
-+ // this bitset represents the values that we have not propagated to
-+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
-+ // significantly reducing the total number of ops
-+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
-+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
-+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
-+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
-+ // index = x | (z << 3)
-+
-+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
-+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
-+ // but the rest not propagated are already handled
-+ long currentPropagation = ~(
-+ // z = -1
-+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
-+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
-+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
-+
-+ // z = 0
-+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
-+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
-+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
-+
-+ // z = 1
-+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
-+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
-+ (1L << ((2 + 1) | ((2 + 1) << 3)))
-+ );
-+
-+ final int toPropagate = propagatedLevel - 1;
-+
-+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
-+ // the bits, the cpu loop predictor should perfectly predict the loop.
-+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
-+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
-+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
-+ propagateDirectionBitset ^= tailingBit;
-+
-+
-+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
-+ // it has been split to save some cycles via parallelism
-+ final int pDecodeX = (set & 3);
-+ final int pDecodeZ = ((set >>> 2) & 3);
-+
-+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
-+ final int offX = (posX - 1) + pDecodeX;
-+ final int offZ = (posZ - 1) + pDecodeZ;
-+
-+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
-+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
-+
-+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
-+ // bitset idx = x | (z << 3)
-+
-+ // read three bits, so we need 7L
-+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
-+ // nstartidx1 = x rel -1 for z rel -1
-+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
-+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
-+ // = pDecodeX | (pDecodeZ << 3) = start
-+ final int start = pDecodeX | (pDecodeZ << 3);
-+ final long bitsetLine1 = currentPropagation & (7L << (start));
-+
-+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
-+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
-+
-+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
-+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
-+
-+ // now try to propagate
-+ final Section section = this.sections[sectionIndex];
-+
-+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
-+ final short currentStoredLevel = section.levels[localIndex];
-+ final int currentLevel = currentStoredLevel & 0xFF;
-+ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF;
-+
-+ if (currentLevel == 0) {
-+ continue; // already at the level we want
-+ }
-+
-+ if (currentLevel > toPropagate) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
-+ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
-+ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
-+ continue;
-+ }
-+
-+ // remove ("take") lines from bitset
-+ // can't do this during decrease, TODO WHY?
-+ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
-+
-+ // update level
-+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF));
-+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0);
-+
-+ if (sourceLevel != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
-+ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
-+ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
-+ }
-+
-+ // queue next
-+ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0
-+ // now combine into one bitset to pass to child
-+ // the child bitset is 4x4, so we just shift each line by 4
-+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
-+ final long childPropagation =
-+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
-+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
-+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
-+
-+ // don't queue update if toPropagate cannot propagate anything to neighbours
-+ // (for increase, propagating 0 to neighbours is useless)
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
-+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
-+ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation;
-+ continue;
-+ }
-+ }
-+
-+ // propagate sources we clobbered
-+ this.increaseQueueInitialLength = increaseQueueLength;
-+ this.performIncrease();
-+ }
-+ }
-+
-+ /*
-+ private static final java.util.Random random = new java.util.Random(4L);
-+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers =
-+ new java.util.ArrayList<>();
-+ static final int PLAYERS = 0;
-+ static final int RAD_BLOCKS = 10000;
-+ static final int RAD = RAD_BLOCKS >> 4;
-+ static final int RAD_BIG_BLOCKS = 100_000;
-+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
-+ static final int VD = 4;
-+ static final int BIG_PLAYERS = 50;
-+ static final double WALK_CHANCE = 0.10;
-+ static final double TP_CHANCE = 0.01;
-+ static final int TP_BACK_PLAYERS = 200;
-+ static final double TP_BACK_CHANCE = 0.25;
-+ static final double TP_STEAL_CHANCE = 0.25;
-+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> tpBack =
-+ new java.util.ArrayList<>();
-+
-+ public static void main(final String[] args) {
-+ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT);
-+ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT);
-+ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap();
-+ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap();
-+ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> {
-+ if (newLevel == 0) {
-+ refMap.remove(coordinate);
-+ } else {
-+ refMap.put(coordinate, newLevel);
-+ }
-+ });
-+ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() {
-+ @Override
-+ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) {
-+ for (final long key : updates.keySet()) {
-+ final byte val = updates.get(key);
-+ if (val == 0) {
-+ levelMap.remove(key);
-+ } else {
-+ levelMap.put(key, val);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List<ChunkProgressionTask> scheduledTasks, List<NewChunkHolder> changedFullStatus) {}
-+ };
-+
-+ for (;;) {
-+ if (walkers.isEmpty() && tpBack.isEmpty()) {
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
-+ int posX = random.nextInt(-rad, rad + 1);
-+ int posZ = random.nextInt(-rad, rad + 1);
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
-+ @Override
-+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
-+ int src = 45 - 31 + 1;
-+ ref.setSource(chunkX, chunkZ, src);
-+ propagator.setSource(chunkX, chunkZ, src);
-+ }
-+
-+ @Override
-+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
-+ ref.removeSource(chunkX, chunkZ);
-+ propagator.removeSource(chunkX, chunkZ);
-+ }
-+ };
-+
-+ map.add(posX, posZ, VD);
-+
-+ walkers.add(map);
-+ }
-+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
-+ int rad = RAD_BIG;
-+ int posX = random.nextInt(-rad, rad + 1);
-+ int posZ = random.nextInt(-rad, rad + 1);
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
-+ @Override
-+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
-+ int src = 45 - 31 + 1;
-+ ref.setSource(chunkX, chunkZ, src);
-+ propagator.setSource(chunkX, chunkZ, src);
-+ }
-+
-+ @Override
-+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
-+ ref.removeSource(chunkX, chunkZ);
-+ propagator.removeSource(chunkX, chunkZ);
-+ }
-+ };
-+
-+ map.add(posX, posZ, random.nextInt(1, 63));
-+
-+ tpBack.add(map);
-+ }
-+ } else {
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ if (random.nextDouble() > WALK_CHANCE) {
-+ continue;
-+ }
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
-+
-+ int updateX = random.nextInt(-1, 2);
-+ int updateZ = random.nextInt(-1, 2);
-+
-+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
-+ }
-+
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ if (random.nextDouble() > TP_CHANCE) {
-+ continue;
-+ }
-+
-+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
-+ int posX = random.nextInt(-rad, rad + 1);
-+ int posZ = random.nextInt(-rad, rad + 1);
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
-+
-+ map.update(posX, posZ, VD);
-+ }
-+
-+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
-+ if (random.nextDouble() > TP_BACK_CHANCE) {
-+ continue;
-+ }
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = tpBack.get(i);
-+
-+ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63));
-+
-+ if (random.nextDouble() > TP_STEAL_CHANCE) {
-+ propagator.performUpdate(
-+ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null
-+ );
-+ propagator.performUpdate(
-+ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null
-+ );
-+ }
-+ }
-+ }
-+
-+ ref.propagateUpdates();
-+ propagator.performUpdates(ticketLock, schedulingLock, null, null);
-+
-+ if (!refMap.equals(levelMap)) {
-+ throw new IllegalStateException("Error!");
-+ }
-+ }
-+ }
-+ */
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-@@ -0,0 +1,729 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
-+
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import java.util.ArrayList;
-+import java.util.Comparator;
-+import java.util.List;
-+import java.util.PriorityQueue;
-+
-+public class RadiusAwarePrioritisedExecutor {
-+
-+ private static final Comparator<DependencyNode> DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> {
-+ return Long.compare(t1.id, t2.id);
-+ };
-+
-+ private final PrioritisedExecutor executor;
-+ private final DependencyTree[] queues = new DependencyTree[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
-+ private static final int NO_TASKS_QUEUED = -1;
-+ private int selectedQueue = NO_TASKS_QUEUED;
-+ private boolean canQueueTasks = true;
-+
-+ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) {
-+ this.executor = executor;
-+
-+ for (int i = 0; i < this.queues.length; ++i) {
-+ this.queues[i] = new DependencyTree(this, executor, maxToSchedule);
-+ }
-+ }
-+
-+ public void setMaxToSchedule(final int maxToSchedule) {
-+ final List<PrioritisedExecutor.PrioritisedTask> tasks;
-+
-+ synchronized (this) {
-+ for (final DependencyTree dependencyTree : this.queues) {
-+ dependencyTree.maxToSchedule = maxToSchedule;
-+ }
-+
-+ if (this.selectedQueue == NO_TASKS_QUEUED || !this.canQueueTasks) {
-+ return;
-+ }
-+
-+ tasks = this.queues[this.selectedQueue].tryPushTasks();
-+ }
-+
-+ scheduleTasks(tasks);
-+ }
-+
-+ private boolean canQueueTasks() {
-+ return this.canQueueTasks;
-+ }
-+
-+ private List<PrioritisedExecutor.PrioritisedTask> treeFinished() {
-+ this.canQueueTasks = true;
-+ for (int priority = 0; priority < this.queues.length; ++priority) {
-+ final DependencyTree queue = this.queues[priority];
-+ if (queue.hasWaitingTasks()) {
-+ final List<PrioritisedExecutor.PrioritisedTask> ret = queue.tryPushTasks();
-+
-+ if (ret == null || ret.isEmpty()) {
-+ // this happens when the tasks in the wait queue were purged
-+ // in this case, the queue was actually empty, we just had to purge it
-+ // if we set the selected queue without scheduling any tasks, the queue will never be unselected
-+ // as that requires a scheduled task completing...
-+ continue;
-+ }
-+
-+ this.selectedQueue = priority;
-+ return ret;
-+ }
-+ }
-+
-+ this.selectedQueue = NO_TASKS_QUEUED;
-+
-+ return null;
-+ }
-+
-+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final Priority priority) {
-+ final int priorityId = priority.priority;
-+ final DependencyTree queue = this.queues[priorityId];
-+
-+ final DependencyNode node = new DependencyNode(task, queue);
-+
-+ if (task.dependencyNode != null) {
-+ throw new IllegalStateException();
-+ }
-+ task.dependencyNode = node;
-+
-+ queue.pushNode(node);
-+
-+ if (this.selectedQueue == NO_TASKS_QUEUED) {
-+ this.canQueueTasks = true;
-+ this.selectedQueue = priorityId;
-+ return queue.tryPushTasks();
-+ }
-+
-+ if (!this.canQueueTasks) {
-+ return null;
-+ }
-+
-+ if (Priority.isHigherPriority(priorityId, this.selectedQueue)) {
-+ // prevent the lower priority tree from queueing more tasks
-+ this.canQueueTasks = false;
-+ return null;
-+ }
-+
-+ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up
-+ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run, final Priority priority) {
-+ if (radius < 0) {
-+ throw new IllegalArgumentException("Radius must be > 0: " + radius);
-+ }
-+ return new Task(this, chunkX, chunkZ, radius, run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run) {
-+ return this.createTask(chunkX, chunkZ, radius, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run, final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final Priority priority) {
-+ return new Task(this, 0, 0, -1, run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) {
-+ return this.createInfiniteRadiusTask(run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, Priority.NORMAL);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) {
-+ if (toSchedule != null) {
-+ for (int i = 0, len = toSchedule.size(); i < len; ++i) {
-+ toSchedule.get(i).queue();
-+ }
-+ }
-+ }
-+
-+ // all accesses must be synchronised by the radius aware object
-+ private static final class DependencyTree {
-+
-+ private final RadiusAwarePrioritisedExecutor scheduler;
-+ private final PrioritisedExecutor executor;
-+ private int maxToSchedule;
-+
-+ private int currentlyExecuting;
-+ private long idGenerator;
-+
-+ private final PriorityQueue<DependencyNode> awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
-+
-+ private final PriorityQueue<DependencyNode> infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
-+ private boolean isInfiniteRadiusScheduled;
-+
-+ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>();
-+
-+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor,
-+ final int maxToSchedule) {
-+ this.scheduler = scheduler;
-+ this.executor = executor;
-+ this.maxToSchedule = maxToSchedule;
-+ }
-+
-+ public boolean hasWaitingTasks() {
-+ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty();
-+ }
-+
-+ private long nextId() {
-+ return this.idGenerator++;
-+ }
-+
-+ private boolean isExecutingAnyTasks() {
-+ return this.currentlyExecuting != 0;
-+ }
-+
-+ private void pushNode(final DependencyNode node) {
-+ if (!node.task.isFiniteRadius()) {
-+ this.infiniteRadius.add(node);
-+ return;
-+ }
-+
-+ // set up dependency for node
-+ final Task task = node.task;
-+
-+ final int centerX = task.chunkX;
-+ final int centerZ = task.chunkZ;
-+ final int radius = task.radius;
-+
-+ final int minX = centerX - radius;
-+ final int maxX = centerX + radius;
-+
-+ final int minZ = centerZ - radius;
-+ final int maxZ = centerZ + radius;
-+
-+ ReferenceOpenHashSet<DependencyNode> parents = null;
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node);
-+ if (dependency != null) {
-+ if (parents == null) {
-+ parents = new ReferenceOpenHashSet<>();
-+ }
-+ if (parents.add(dependency)) {
-+ // added a dependency, so we need to add as a child to the dependency
-+ if (dependency.children == null) {
-+ dependency.children = new ArrayList<>();
-+ }
-+ dependency.children.add(node);
-+ }
-+ }
-+ }
-+ }
-+
-+ if (parents == null) {
-+ // no dependencies, add straight to awaiting
-+ this.awaiting.add(node);
-+ } else {
-+ node.parents = parents.size();
-+ // we will be added to awaiting once we have no parents
-+ }
-+ }
-+
-+ // called only when a node is returned after being executed
-+ private List<PrioritisedExecutor.PrioritisedTask> returnNode(final DependencyNode node) {
-+ final Task task = node.task;
-+
-+ // now that the task is completed, we can push its children to the awaiting queue
-+ this.pushChildren(node);
-+
-+ if (task.isFiniteRadius()) {
-+ // remove from dependency map
-+ this.removeNodeFromMap(node);
-+ } else {
-+ // mark as no longer executing infinite radius
-+ if (!this.isInfiniteRadiusScheduled) {
-+ throw new IllegalStateException();
-+ }
-+ this.isInfiniteRadiusScheduled = false;
-+ }
-+
-+ // decrement executing count, we are done executing this task
-+ --this.currentlyExecuting;
-+
-+ if (this.currentlyExecuting == 0) {
-+ return this.scheduler.treeFinished();
-+ }
-+
-+ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null;
-+ }
-+
-+ private List<PrioritisedExecutor.PrioritisedTask> tryPushTasks() {
-+ // tasks are not queued, but only created here - we do hold the lock for the map
-+ List<PrioritisedExecutor.PrioritisedTask> ret = null;
-+ PrioritisedExecutor.PrioritisedTask pushedTask;
-+ while ((pushedTask = this.tryPushTask()) != null) {
-+ if (ret == null) {
-+ ret = new ArrayList<>();
-+ }
-+ ret.add(pushedTask);
-+ }
-+
-+ return ret;
-+ }
-+
-+ private void removeNodeFromMap(final DependencyNode node) {
-+ final Task task = node.task;
-+
-+ final int centerX = task.chunkX;
-+ final int centerZ = task.chunkZ;
-+ final int radius = task.radius;
-+
-+ final int minX = centerX - radius;
-+ final int maxX = centerX + radius;
-+
-+ final int minZ = centerZ - radius;
-+ final int maxZ = centerZ + radius;
-+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node);
-+ }
-+ }
-+ }
-+
-+ private void pushChildren(final DependencyNode node) {
-+ // add all the children that we can into awaiting
-+ final List<DependencyNode> children = node.children;
-+ if (children != null) {
-+ for (int i = 0, len = children.size(); i < len; ++i) {
-+ final DependencyNode child = children.get(i);
-+ int newParents = --child.parents;
-+ if (newParents == 0) {
-+ // no more dependents, we can push to awaiting
-+ // even if the child is purged, we need to push it so that its children will be pushed
-+ this.awaiting.add(child);
-+ } else if (newParents < 0) {
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+ }
-+
-+ private DependencyNode pollAwaiting() {
-+ final DependencyNode ret = this.awaiting.poll();
-+ if (ret == null) {
-+ return ret;
-+ }
-+
-+ if (ret.parents != 0) {
-+ throw new IllegalStateException();
-+ }
-+
-+ if (ret.purged) {
-+ // need to manually remove from state here
-+ this.pushChildren(ret);
-+ this.removeNodeFromMap(ret);
-+ } // else: delay children push until the task has finished
-+
-+ return ret;
-+ }
-+
-+ private DependencyNode pollInfinite() {
-+ return this.infiniteRadius.poll();
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask tryPushTask() {
-+ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) {
-+ return null;
-+ }
-+
-+ DependencyNode firstInfinite;
-+ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) {
-+ this.pollInfinite();
-+ }
-+
-+ DependencyNode firstAwaiting;
-+ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) {
-+ this.pollAwaiting();
-+ }
-+
-+ if (firstInfinite == null && firstAwaiting == null) {
-+ return null;
-+ }
-+
-+ // firstAwaiting compared to firstInfinite
-+ final int compare;
-+
-+ if (firstAwaiting == null) {
-+ // we choose first infinite, or infinite < awaiting
-+ compare = 1;
-+ } else if (firstInfinite == null) {
-+ // we choose first awaiting, or awaiting < infinite
-+ compare = -1;
-+ } else {
-+ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite);
-+ }
-+
-+ if (compare >= 0) {
-+ if (this.currentlyExecuting != 0) {
-+ // don't queue infinite task while other tasks are executing in parallel
-+ return null;
-+ }
-+ ++this.currentlyExecuting;
-+ this.pollInfinite();
-+ this.isInfiniteRadiusScheduled = true;
-+ return firstInfinite.task.pushTask(this.executor);
-+ } else {
-+ ++this.currentlyExecuting;
-+ this.pollAwaiting();
-+ return firstAwaiting.task.pushTask(this.executor);
-+ }
-+ }
-+ }
-+
-+ private static final class DependencyNode {
-+
-+ private final Task task;
-+ private final DependencyTree tree;
-+
-+ // dependency tree fields
-+ // (must hold lock on the scheduler to use)
-+ // null is the same as empty, we just use it so that we don't allocate the set unless we need to
-+ private List<DependencyNode> children;
-+ // 0 indicates that this task is considered "awaiting"
-+ private int parents;
-+ // false -> scheduled and not cancelled
-+ // true -> scheduled but cancelled
-+ private boolean purged;
-+ private final long id;
-+
-+ public DependencyNode(final Task task, final DependencyTree tree) {
-+ this.task = task;
-+ this.id = tree.nextId();
-+ this.tree = tree;
-+ }
-+ }
-+
-+ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable {
-+
-+ // task specific fields
-+ private final RadiusAwarePrioritisedExecutor scheduler;
-+ private final int chunkX;
-+ private final int chunkZ;
-+ private final int radius;
-+ private Runnable run;
-+ private Priority priority;
-+
-+ private DependencyNode dependencyNode;
-+ private PrioritisedExecutor.PrioritisedTask queuedTask;
-+
-+ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run, final Priority priority) {
-+ this.scheduler = scheduler;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.radius = radius;
-+ this.run = run;
-+ this.priority = priority;
-+ }
-+
-+ private boolean isFiniteRadius() {
-+ return this.radius >= 0;
-+ }
-+
-+ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) {
-+ return this.queuedTask = executor.createTask(this, this.priority);
-+ }
-+
-+ private void executeTask() {
-+ final Runnable run = this.run;
-+ this.run = null;
-+ run.run();
-+ }
-+
-+ private void returnNode() {
-+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
-+ synchronized (this.scheduler) {
-+ final DependencyNode node = this.dependencyNode;
-+ this.dependencyNode = null;
-+ toSchedule = node.tree.returnNode(node);
-+ }
-+
-+ scheduleTasks(toSchedule);
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor getExecutor() {
-+ return this.scheduler.executor;
-+ }
-+
-+ @Override
-+ public void run() {
-+ final Runnable run = this.run;
-+ this.run = null;
-+ try {
-+ run.run();
-+ } finally {
-+ this.returnNode();
-+ }
-+ }
-+
-+ @Override
-+ public boolean queue() {
-+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
-+ synchronized (this.scheduler) {
-+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ toSchedule = this.scheduler.queue(this, this.priority);
-+ }
-+
-+ scheduleTasks(toSchedule);
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean isQueued() {
-+ synchronized (this.scheduler) {
-+ return (this.queuedTask != null || this.dependencyNode != null) && this.priority != Priority.COMPLETING;
-+ }
-+ }
-+
-+ @Override
-+ public boolean cancel() {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ this.priority = Priority.COMPLETING;
-+ if (this.dependencyNode != null) {
-+ this.dependencyNode.purged = true;
-+ this.dependencyNode = null;
-+ }
-+
-+ return true;
-+ }
-+ }
-+
-+ if (task.cancel()) {
-+ // must manually return the node
-+ this.run = null;
-+ this.returnNode();
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ @Override
-+ public boolean execute() {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ this.priority = Priority.COMPLETING;
-+ if (this.dependencyNode != null) {
-+ this.dependencyNode.purged = true;
-+ this.dependencyNode = null;
-+ }
-+ // fall through to execution logic
-+ }
-+ }
-+
-+ if (task != null) {
-+ // will run the return node logic automatically
-+ return task.execute();
-+ } else {
-+ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree
-+ this.executeTask();
-+ return true;
-+ }
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ return this.priority;
-+ }
-+ }
-+
-+ return task.getPriority();
-+ }
-+
-+ @Override
-+ public boolean setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (this.priority == priority) {
-+ return true;
-+ }
-+
-+ this.priority = priority;
-+ if (this.dependencyNode != null) {
-+ // need to re-insert node
-+ this.dependencyNode.purged = true;
-+ this.dependencyNode = null;
-+ toSchedule = this.scheduler.queue(this, priority);
-+ }
-+ }
-+ }
-+
-+ if (task != null) {
-+ return task.setPriority(priority);
-+ }
-+
-+ scheduleTasks(toSchedule);
-+
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (this.priority.isHigherOrEqualPriority(priority)) {
-+ return true;
-+ }
-+
-+ this.priority = priority;
-+ if (this.dependencyNode != null) {
-+ // need to re-insert node
-+ this.dependencyNode.purged = true;
-+ this.dependencyNode = null;
-+ toSchedule = this.scheduler.queue(this, priority);
-+ }
-+ }
-+ }
-+
-+ if (task != null) {
-+ return task.raisePriority(priority);
-+ }
-+
-+ scheduleTasks(toSchedule);
-+
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ final PrioritisedExecutor.PrioritisedTask task;
-+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
-+ synchronized (this.scheduler) {
-+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (this.priority.isLowerOrEqualPriority(priority)) {
-+ return true;
-+ }
-+
-+ this.priority = priority;
-+ if (this.dependencyNode != null) {
-+ // need to re-insert node
-+ this.dependencyNode.purged = true;
-+ this.dependencyNode = null;
-+ toSchedule = this.scheduler.queue(this, priority);
-+ }
-+ }
-+ }
-+
-+ if (task != null) {
-+ return task.lowerPriority(priority);
-+ }
-+
-+ scheduleTasks(toSchedule);
-+
-+ return true;
-+ }
-+
-+ @Override
-+ public long getSubOrder() {
-+ // TODO implement
-+ return 0;
-+ }
-+
-+ @Override
-+ public boolean setSubOrder(final long subOrder) {
-+ // TODO implement
-+ return false;
-+ }
-+
-+ @Override
-+ public boolean raiseSubOrder(final long subOrder) {
-+ // TODO implement
-+ return false;
-+ }
-+
-+ @Override
-+ public boolean lowerSubOrder(final long subOrder) {
-+ // TODO implement
-+ return false;
-+ }
-+
-+ @Override
-+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
-+ // TODO implement
-+ return this.setPriority(priority);
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-@@ -0,0 +1,151 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+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.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ImposterProtoChunk;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.lang.invoke.VarHandle;
-+
-+public final class ChunkFullTask extends ChunkProgressionTask implements Runnable {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class);
-+
-+ private final NewChunkHolder chunkHolder;
-+ private final ChunkAccess fromChunk;
-+ private final PrioritisedExecutor.PrioritisedTask convertToFullTask;
-+
-+ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ this.chunkHolder = chunkHolder;
-+ this.fromChunk = fromChunk;
-+ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority);
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.FULL;
-+ }
-+
-+ @Override
-+ public void run() {
-+ final PlatformHooks platformHooks = PlatformHooks.get();
-+
-+ // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing
-+ final LevelChunk chunk;
-+ try {
-+ // moved from the load from nbt stage into here
-+ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk();
-+ if (poiChunk == null) {
-+ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
-+ } else {
-+ poiChunk.load();
-+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk);
-+ }
-+
-+ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) {
-+ chunk = wrappedFull.getWrapped();
-+ } else {
-+ final ServerLevel world = this.world;
-+ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
-+ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
-+ PlatformHooks.get().postLoadProtoChunk(world, protoChunk);
-+ });
-+ 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);
-+ try {
-+ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, chunk);
-+ chunk.runPostLoad();
-+ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla)
-+ // This brings entity addition back in line with older versions of the game
-+ // Since we load the NBT in the empty status, this will never block for I/O
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false);
-+ chunk.setLoaded(true);
-+ chunk.registerAllBlockEntitiesAfterLevelLoad();
-+ chunk.registerTickContainerInLevel(this.world);
-+ chunk.setUnsavedListener(this.world.getChunkSource().chunkMap.worldGenContext.unsavedListener());
-+ platformHooks.chunkFullStatusComplete(chunk, (ProtoChunk)this.fromChunk);
-+ } finally {
-+ platformHooks.setCurrentlyLoading(this.chunkHolder.vanillaChunkHolder, null);
-+ }
-+ } catch (final Throwable throwable) {
-+ this.complete(null, throwable);
-+ return;
-+ }
-+ this.complete(chunk, null);
-+ }
-+
-+ protected volatile boolean scheduled;
-+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class);
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) {
-+ throw new IllegalStateException("Cannot double call schedule()");
-+ }
-+ this.convertToFullTask.queue();
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ if (this.convertToFullTask.cancel()) {
-+ this.complete(null, null);
-+ }
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.convertToFullTask.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.convertToFullTask.lowerPriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.convertToFullTask.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.convertToFullTask.raisePriority(priority);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-@@ -0,0 +1,181 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder;
-+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
-+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
-+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import org.apache.logging.log4j.LogManager;
-+import org.apache.logging.log4j.Logger;
-+import java.util.function.BooleanSupplier;
-+
-+public final class ChunkLightTask extends ChunkProgressionTask {
-+
-+ private static final Logger LOGGER = LogManager.getLogger();
-+
-+ private final ChunkAccess fromChunk;
-+
-+ private final LightTaskPriorityHolder priorityHolder;
-+
-+ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final ChunkAccess chunk, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
-+ this.fromChunk = chunk;
-+ }
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.priorityHolder.isScheduled();
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.LIGHT;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ this.priorityHolder.schedule();
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ this.priorityHolder.cancel();
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.priorityHolder.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final Priority priority) {
-+ this.priorityHolder.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final Priority priority) {
-+ this.priorityHolder.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final Priority priority) {
-+ this.priorityHolder.raisePriority(priority);
-+ }
-+
-+ private static final class LightTaskPriorityHolder extends PriorityHolder {
-+
-+ private final ChunkLightTask task;
-+
-+ private LightTaskPriorityHolder(final Priority priority, final ChunkLightTask task) {
-+ super(priority);
-+ this.task = task;
-+ }
-+
-+ @Override
-+ protected void cancelScheduled() {
-+ final ChunkLightTask task = this.task;
-+ task.complete(null, null);
-+ }
-+
-+ @Override
-+ protected Priority getScheduledPriority() {
-+ final ChunkLightTask task = this.task;
-+ return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ);
-+ }
-+
-+ @Override
-+ protected void scheduleTask(final Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
-+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
-+ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority);
-+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void lowerPriorityScheduled(final Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
-+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
-+ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void setPriorityScheduled(final Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
-+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
-+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
-+ }
-+
-+ @Override
-+ protected void raisePriorityScheduled(final Priority priority) {
-+ final ChunkLightTask task = this.task;
-+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
-+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
-+ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority);
-+ }
-+ }
-+
-+ private static final class LightTask implements BooleanSupplier {
-+
-+ private final StarLightInterface lightEngine;
-+ private final ChunkLightTask task;
-+
-+ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) {
-+ this.lightEngine = lightEngine;
-+ this.task = task;
-+ }
-+
-+ @Override
-+ public boolean getAsBoolean() {
-+ final ChunkLightTask task = this.task;
-+ // executed on light thread
-+ if (!task.priorityHolder.markExecuting()) {
-+ // cancelled
-+ return false;
-+ }
-+
-+ try {
-+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk);
-+
-+ if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections);
-+ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ);
-+ } else {
-+ task.fromChunk.setLightCorrect(false);
-+ this.lightEngine.lightChunk(task.fromChunk, emptySections);
-+ task.fromChunk.setLightCorrect(true);
-+ }
-+ // we need to advance status
-+ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) {
-+ chunk.setPersistedStatus(ChunkStatus.LIGHT);
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.fatal(
-+ "Failed to light chunk " + task.fromChunk.getPos().toString()
-+ + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr
-+ );
-+
-+ task.complete(null, thr);
-+
-+ return true;
-+ }
-+
-+ task.complete(task.fromChunk, null);
-+ return true;
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1440c9e2b106616884edcb20201113320817ed9f
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-@@ -0,0 +1,494 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
-+import net.minecraft.core.registries.Registries;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.UpgradeData;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
-+import net.minecraft.world.level.levelgen.blending.BlendingData;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.function.Consumer;
-+
-+public final class ChunkLoadTask extends ChunkProgressionTask {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class);
-+
-+ private final NewChunkHolder chunkHolder;
-+ private final ChunkDataLoadTask loadTask;
-+
-+ private volatile boolean cancelled;
-+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
-+ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
-+ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult;
-+ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data
-+
-+ public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final NewChunkHolder chunkHolder, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ this.chunkHolder = chunkHolder;
-+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
-+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> {
-+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
-+ ChunkLoadTask.this.tryCompleteLoad();
-+ });
-+ }
-+
-+ private void tryCompleteLoad() {
-+ final int count = this.taskCountToComplete.decrementAndGet();
-+ if (count == 0) {
-+ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement
-+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
-+ } else if (count < 0) {
-+ throw new IllegalStateException("Called tryCompleteLoad() too many times");
-+ }
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return ChunkStatus.EMPTY;
-+ }
-+
-+ private boolean scheduled;
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
-+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
-+
-+ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> {
-+ ChunkLoadTask.this.tryCompleteLoad();
-+ };
-+
-+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because
-+ // they must schedule a task to off main or to on main to complete
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ if (this.scheduled) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+ this.scheduled = true;
-+ if (this.cancelled) {
-+ return;
-+ }
-+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) {
-+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask);
-+ } else {
-+ entityLoadTask = null;
-+ this.tryCompleteLoad();
-+ }
-+
-+ if (!this.chunkHolder.isPoiChunkLoaded()) {
-+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask);
-+ } else {
-+ poiLoadTask = null;
-+ this.tryCompleteLoad();
-+ }
-+
-+ this.entityLoadTask = entityLoadTask;
-+ this.poiLoadTask = poiLoadTask;
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ if (entityLoadTask != null) {
-+ entityLoadTask.schedule();
-+ }
-+
-+ if (poiLoadTask != null) {
-+ poiLoadTask.schedule();
-+ }
-+
-+ this.loadTask.schedule(false);
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ // must be before load task access, so we can synchronise with the writes to the fields
-+ final boolean scheduled;
-+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
-+ try {
-+ // must read field here, as it may be written later conucrrently -
-+ // we need to know if we scheduled _before_ cancellation
-+ scheduled = this.scheduled;
-+ this.cancelled = true;
-+ } finally {
-+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
-+ }
-+
-+ /*
-+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled,
-+ so we need to manually try to complete in those cases
-+ It is also important to note that we set the cancelled field first, just in case
-+ the chunk load task attempts to complete with a non-null value
-+ */
-+
-+ if (scheduled) {
-+ // since we scheduled, we need to cancel the tasks
-+ if (this.entityLoadTask != null) {
-+ if (this.entityLoadTask.cancel()) {
-+ this.tryCompleteLoad();
-+ }
-+ }
-+ if (this.poiLoadTask != null) {
-+ if (this.poiLoadTask.cancel()) {
-+ this.tryCompleteLoad();
-+ }
-+ }
-+ } else {
-+ // since nothing was scheduled, we need to decrement the task count here ourselves
-+
-+ // for entity load task
-+ this.tryCompleteLoad();
-+
-+ // for poi load task
-+ this.tryCompleteLoad();
-+ }
-+ this.loadTask.cancel();
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.loadTask.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.lowerPriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.lowerPriority(priority);
-+ }
-+
-+ this.loadTask.lowerPriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.setPriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.setPriority(priority);
-+ }
-+
-+ this.loadTask.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final Priority priority) {
-+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
-+ if (entityLoad != null) {
-+ entityLoad.raisePriority(priority);
-+ }
-+
-+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
-+
-+ if (poiLoad != null) {
-+ poiLoad.raisePriority(priority);
-+ }
-+
-+ this.loadTask.raisePriority(priority);
-+ }
-+
-+ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> {
-+
-+ private TaskResult<FinalCompletion, Throwable> result;
-+ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>();
-+
-+ protected volatile boolean completed;
-+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class);
-+
-+ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
-+ final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, type, priority);
-+ }
-+
-+ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) {
-+ if (!this.waiters.add(consumer)) {
-+ try {
-+ consumer.accept(this.result);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()),
-+ "CallbackDataLoadTask impl", this.getClass().getName()
-+ ), throwable);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) {
-+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) {
-+ throw new IllegalStateException("Already completed");
-+ }
-+ this.result = result;
-+ Consumer<TaskResult<FinalCompletion, Throwable>> consumer;
-+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
-+ try {
-+ consumer.accept(result);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()),
-+ "CallbackDataLoadTask impl", this.getClass().getName()
-+ ), throwable);
-+ return;
-+ }
-+ }
-+ }
-+ }
-+
-+
-+ private static record ReadChunk(ProtoChunk protoChunk, SerializableChunkData chunkData) {}
-+
-+ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<ReadChunk, ChunkAccess> {
-+ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
-+ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority);
-+ }
-+
-+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) {
-+ if (throwable != null) {
-+ return new TaskResult<>(null, throwable);
-+ }
-+
-+ if (data == null || data.protoChunk() == null) {
-+ return new TaskResult<>(this.getEmptyChunk(), null);
-+ }
-+
-+ if (!PlatformHooks.get().hasMainChunkLoadHook()) {
-+ return new TaskResult<>(data.protoChunk(), null);
-+ }
-+
-+ // need to invoke the callback for loading on the main thread
-+ return null;
-+ }
-+
-+ private ProtoChunk getEmptyChunk() {
-+ return new ProtoChunk(
-+ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world,
-+ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null
-+ );
-+ }
-+
-+ @Override
-+ protected TaskResult<ReadChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable);
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ if (data == null) {
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ try {
-+ // run converters
-+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new ChunkPos(this.chunkX, this.chunkZ)); // Paper
-+
-+ // unpack the data
-+ final SerializableChunkData chunkData = SerializableChunkData.parse(
-+ this.world, this.world.registryAccess(), converted
-+ );
-+
-+ if (chunkData == null) {
-+ LOGGER.error("Deserialized chunk for task: " + this.toString() + " produced null, chunk data will be lost?");
-+ }
-+
-+ // read into ProtoChunk
-+ final ProtoChunk chunk = chunkData == null ? null : chunkData.read(
-+ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(),
-+ new ChunkPos(this.chunkX, this.chunkZ)
-+ );
-+
-+ return new TaskResult<>(new ReadChunk(chunk, chunkData), null);
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
-+ return new TaskResult<>(null, null);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> runOnMain(final ReadChunk data, final Throwable throwable) {
-+ PlatformHooks.get().mainChunkLoad(data.protoChunk(), data.chunkData());
-+
-+ return new TaskResult<>(data.protoChunk(), null);
-+ }
-+ }
-+
-+ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> {
-+
-+ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return false;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable);
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+
-+ if (data == null || data.isEmpty()) {
-+ // nothing to do
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+
-+ try {
-+ // run converters
-+ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world);
-+
-+ // now we need to parse it
-+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null);
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2);
-+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+
-+ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
-+
-+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority);
-+ }
-+
-+ @Override
-+ protected boolean hasOffMain() {
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasOnMain() {
-+ return false;
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority) {
-+ return this.scheduler.loadExecutor.createTask(run, priority);
-+ }
-+
-+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable);
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ if (data == null || data.isEmpty()) {
-+ // nothing to do
-+ return new TaskResult<>(null, null);
-+ }
-+
-+ try {
-+ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null);
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2);
-+ return new TaskResult<>(null, thr2);
-+ }
-+ }
-+
-+ @Override
-+ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
-+ throw new UnsupportedOperationException();
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-@@ -0,0 +1,101 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.function.BiConsumer;
-+
-+public abstract class ChunkProgressionTask {
-+
-+ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>();
-+ private ChunkAccess completedChunk;
-+ private Throwable completedThrowable;
-+
-+ protected final ChunkTaskScheduler scheduler;
-+ protected final ServerLevel world;
-+ protected final int chunkX;
-+ protected final int chunkZ;
-+
-+ protected volatile boolean completed;
-+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class);
-+
-+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) {
-+ this.scheduler = scheduler;
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ }
-+
-+ // Used only for debug json
-+ public abstract boolean isScheduled();
-+
-+ // Note: It is the responsibility of the task to set the chunk's status once it has completed
-+ public abstract ChunkStatus getTargetStatus();
-+
-+ /* Only executed once */
-+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */
-+ public abstract void schedule();
-+
-+ /* May be called multiple times */
-+ public abstract void cancel();
-+
-+ public abstract Priority getPriority();
-+
-+ /* Schedule lock is always held for the priority update calls */
-+
-+ public abstract void lowerPriority(final Priority priority);
-+
-+ public abstract void setPriority(final Priority priority);
-+
-+ public abstract void raisePriority(final Priority priority);
-+
-+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
-+ if (!this.waiters.add(onComplete)) {
-+ try {
-+ onComplete.accept(this.completedChunk, this.completedThrowable);
-+ } catch (final Throwable throwable) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete),
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable)
-+ ), throwable);
-+ }
-+ }
-+ }
-+
-+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) {
-+ try {
-+ this.complete0(chunk, throwable);
-+ } catch (final Throwable thr2) {
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable)
-+ ), thr2);
-+ }
-+ }
-+
-+ private void complete0(final ChunkAccess chunk, final Throwable throwable) {
-+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) {
-+ throw new IllegalStateException("Already completed");
-+ }
-+ this.completedChunk = chunk;
-+ this.completedThrowable = throwable;
-+
-+ BiConsumer<ChunkAccess, Throwable> consumer;
-+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
-+ consumer.accept(chunk, throwable);
-+ }
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) +
-+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
-+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}";
-+ }
-+}
-\ No newline at end of file
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-@@ -0,0 +1,218 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.GenerationChunkHolder;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.util.StaticCache2D;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.status.ChunkPyramid;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.status.WorldGenContext;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.lang.invoke.VarHandle;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.concurrent.CompletableFuture;
-+
-+public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class);
-+
-+ private final ChunkAccess fromChunk;
-+ private final ChunkStatus fromStatus;
-+ private final ChunkStatus toStatus;
-+ private final StaticCache2D<GenerationChunkHolder> neighbours;
-+
-+ private final PrioritisedExecutor.PrioritisedTask generateTask;
-+
-+ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours,
-+ final ChunkStatus toStatus, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ);
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.fromChunk = chunk;
-+ this.fromStatus = chunk.getPersistedStatus();
-+ this.toStatus = toStatus;
-+ this.neighbours = neighbours;
-+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) {
-+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority);
-+ } else {
-+ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius();
-+ if (writeRadius < 0) {
-+ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority);
-+ } else {
-+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public ChunkStatus getTargetStatus() {
-+ return this.toStatus;
-+ }
-+
-+ private boolean isEmptyTask() {
-+ // must use fromStatus here to avoid any race condition with run() overwriting the status
-+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus);
-+ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus());
-+ }
-+
-+ @Override
-+ public void run() {
-+ final ChunkAccess chunk = this.fromChunk;
-+
-+ final ServerChunkCache serverChunkCache = this.world.getChunkSource();
-+ final ChunkMap chunkMap = serverChunkCache.chunkMap;
-+
-+ final CompletableFuture<ChunkAccess> completeFuture;
-+
-+ final boolean generation;
-+ boolean completing = false;
-+
-+ // note: should optimise the case where the chunk does not need to execute the status, because
-+ // schedule() calls this synchronously if it will run through that path
-+
-+ final WorldGenContext ctx = chunkMap.worldGenContext;
-+ try {
-+ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus);
-+ if (generation) {
-+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) {
-+ if (chunk instanceof ProtoChunk) {
-+ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus);
-+ }
-+ completing = true;
-+ this.complete(chunk, null);
-+ return;
-+ }
-+ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk)
-+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> {
-+ if (either instanceof ProtoChunk proto) {
-+ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus);
-+ }
-+ }
-+ );
-+ } else {
-+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) {
-+ completing = true;
-+ this.complete(chunk, null);
-+ return;
-+ }
-+ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk);
-+ }
-+ } catch (final Throwable throwable) {
-+ if (!completing) {
-+ this.complete(null, throwable);
-+ return;
-+ }
-+
-+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus),
-+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus),
-+ "Generation task", this
-+ ), throwable);
-+
-+ LOGGER.error(
-+ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX +
-+ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world),
-+ throwable
-+ );
-+
-+ return;
-+ }
-+
-+ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) {
-+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation);
-+ }
-+
-+ final ChunkAccess newChunk;
-+
-+ try {
-+ newChunk = completeFuture.join();
-+ } catch (final Throwable throwable) {
-+ this.complete(null, throwable);
-+ return;
-+ }
-+
-+ if (newChunk == null) {
-+ this.complete(null,
-+ new IllegalStateException(
-+ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString()
-+ + ", generation: " + generation + " should not be null! Future: " + completeFuture
-+ ).fillInStackTrace()
-+ );
-+ return;
-+ }
-+
-+ this.complete(newChunk, null);
-+ }
-+
-+ private volatile boolean scheduled;
-+ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class);
-+
-+ @Override
-+ public boolean isScheduled() {
-+ return this.scheduled;
-+ }
-+
-+ @Override
-+ public void schedule() {
-+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) {
-+ throw new IllegalStateException("Cannot double call schedule()");
-+ }
-+ if (this.isEmptyTask()) {
-+ if (this.generateTask.cancel()) {
-+ this.run();
-+ }
-+ } else {
-+ this.generateTask.queue();
-+ }
-+ }
-+
-+ @Override
-+ public void cancel() {
-+ if (this.generateTask.cancel()) {
-+ this.complete(null, null);
-+ }
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.generateTask.getPriority();
-+ }
-+
-+ @Override
-+ public void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.lowerPriority(priority);
-+ }
-+
-+ @Override
-+ public void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.setPriority(priority);
-+ }
-+
-+ @Override
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.generateTask.raisePriority(priority);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-@@ -0,0 +1,674 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-+
-+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
-+import ca.spottedleaf.concurrentutil.completable.Completable;
-+import ca.spottedleaf.concurrentutil.executor.Cancellable;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import java.lang.invoke.VarHandle;
-+import java.util.Map;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.BiConsumer;
-+
-+public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
-+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class);
-+
-+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag();
-+
-+ // reference count is the upper 32 bits
-+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED);
-+
-+ protected static final long STAGE_MASK = 0xFFFFFFFFL;
-+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL;
-+ protected static final long STAGE_NOT_STARTED = 0L;
-+ protected static final long STAGE_LOADING = 1L;
-+ protected static final long STAGE_PROCESSING = 2L;
-+ protected static final long STAGE_COMPLETED = 3L;
-+
-+ // for loading data off disk
-+ protected final LoadDataFromDiskTask loadDataFromDiskTask;
-+ // processing off-main
-+ protected final PrioritisedExecutor.PrioritisedTask processOffMain;
-+ // processing on-main
-+ protected final PrioritisedExecutor.PrioritisedTask processOnMain;
-+
-+ protected final ChunkTaskScheduler scheduler;
-+ protected final ServerLevel world;
-+ protected final int chunkX;
-+ protected final int chunkZ;
-+ protected final MoonriseRegionFileIO.RegionFileType type;
-+
-+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
-+ final Priority priority) {
-+ this.scheduler = scheduler;
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.type = type;
-+
-+ final ProcessOnMainTask mainTask;
-+ if (this.hasOnMain()) {
-+ mainTask = new ProcessOnMainTask();
-+ this.processOnMain = this.createOnMain(mainTask, priority);
-+ } else {
-+ mainTask = null;
-+ this.processOnMain = null;
-+ }
-+
-+ final ProcessOffMainTask offMainTask;
-+ if (this.hasOffMain()) {
-+ offMainTask = new ProcessOffMainTask(mainTask);
-+ this.processOffMain = this.createOffMain(offMainTask, priority);
-+ } else {
-+ offMainTask = null;
-+ this.processOffMain = null;
-+ }
-+
-+ if (this.processOffMain == null && this.processOnMain == null) {
-+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!");
-+ }
-+
-+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority);
-+ }
-+
-+ public static final record TaskResult<L, R>(L left, R right) {}
-+
-+ protected abstract boolean hasOffMain();
-+
-+ protected abstract boolean hasOnMain();
-+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority);
-+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority);
-+
-+ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable);
-+
-+ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable);
-+
-+ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result);
-+
-+ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable);
-+
-+ @Override
-+ public String toString() {
-+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) +
-+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
-+ ", type: " + this.type.toString() + "}";
-+ }
-+
-+ public Priority getPriority() {
-+ if (this.processOnMain != null) {
-+ return this.processOnMain.getPriority();
-+ } else {
-+ return this.processOffMain.getPriority();
-+ }
-+ }
-+
-+ public void lowerPriority(final Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ if (this.processOffMain != null) {
-+ this.processOffMain.lowerPriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.lowerPriority(priority);
-+ }
-+ }
-+
-+ public void setPriority(final Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ this.loadDataFromDiskTask.raisePriority(priority);
-+ if (this.processOffMain != null) {
-+ this.processOffMain.setPriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.setPriority(priority);
-+ }
-+ }
-+
-+ public void raisePriority(final Priority priority) {
-+ // can't lower I/O tasks, we don't know what they affect
-+ this.loadDataFromDiskTask.raisePriority(priority);
-+ if (this.processOffMain != null) {
-+ this.processOffMain.raisePriority(priority);
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.raisePriority(priority);
-+ }
-+ }
-+
-+ // returns whether scheduleNow() needs to be called
-+ public boolean schedule(final boolean delay) {
-+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED ||
-+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) {
-+ // try and increment reference count
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) {
-+ // cancelled or completed, nothing to do here
-+ return false;
-+ }
-+
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) {
-+ // successful
-+ return false;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ if (!delay) {
-+ this.scheduleNow();
-+ return false;
-+ }
-+ return true;
-+ }
-+
-+ public void scheduleNow() {
-+ this.loadDataFromDiskTask.schedule(); // will schedule the rest
-+ }
-+
-+ // assumes the current stage cannot be completed
-+ // returns false if cancelled, returns true if can proceed
-+ private boolean advanceStage(final long expect, final long to) {
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) != expect) {
-+ // must be cancelled
-+ return false;
-+ }
-+
-+ final long newVal = (curr & ~STAGE_MASK) | to;
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ return true;
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public boolean cancel() {
-+ int failures = 0;
-+ for (long curr = this.stageAndReferenceCount.get();;) {
-+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) {
-+ return false;
-+ }
-+
-+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) {
-+ // no other references, so we can cancel
-+ final long newVal = STAGE_CANCELLED;
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ this.loadDataFromDiskTask.cancel();
-+ if (this.processOffMain != null) {
-+ this.processOffMain.cancel();
-+ }
-+ if (this.processOnMain != null) {
-+ this.processOnMain.cancel();
-+ }
-+ this.onComplete(null);
-+ return true;
-+ }
-+ } else {
-+ if ((curr & ~STAGE_MASK) == (0L << 32)) {
-+ throw new IllegalStateException("Reference count cannot be zero here");
-+ }
-+ // just decrease the reference count
-+ final long newVal = curr - (1L << 32);
-+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
-+ return false;
-+ }
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> {
-+
-+ private final ProcessOffMainTask offMainTask;
-+ private final ProcessOnMainTask onMainTask;
-+
-+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) {
-+ this.offMainTask = offMainTask;
-+ this.onMainTask = onMainTask;
-+ }
-+
-+ @Override
-+ public void accept(final CompoundTag compoundTag, final Throwable throwable) {
-+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
-+ // don't try to schedule further
-+ return;
-+ }
-+
-+ try {
-+ if (compoundTag == CANCELLED_DATA) {
-+ // cancelled, except this isn't possible
-+ LOGGER.error("Data callback says cancelled, but stage does not?");
-+ return;
-+ }
-+
-+ // get off of the regionfile callback ASAP, no clue what locks are held right now...
-+ if (GenericDataLoadTask.this.processOffMain != null) {
-+ this.offMainTask.data = compoundTag;
-+ this.offMainTask.throwable = throwable;
-+ GenericDataLoadTask.this.processOffMain.queue();
-+ return;
-+ } else {
-+ // no off-main task, so go straight to main
-+ this.onMainTask.data = (OnMain)compoundTag;
-+ this.onMainTask.throwable = throwable;
-+ GenericDataLoadTask.this.processOnMain.queue();
-+ }
-+ } catch (final Throwable thr2) {
-+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2);
-+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure(
-+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of(
-+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable)
-+ ), thr2
-+ );
-+ }
-+ }
-+ }
-+
-+ private final class ProcessOffMainTask implements Runnable {
-+
-+ private CompoundTag data;
-+ private Throwable throwable;
-+ private final ProcessOnMainTask schedule;
-+
-+ public ProcessOffMainTask(final ProcessOnMainTask schedule) {
-+ this.schedule = schedule;
-+ }
-+
-+ @Override
-+ public void run() {
-+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) {
-+ // cancelled
-+ return;
-+ }
-+ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable);
-+
-+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
-+ // don't try to schedule further
-+ return;
-+ }
-+
-+ if (this.schedule != null) {
-+ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right);
-+
-+ if (syncComplete != null) {
-+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
-+ GenericDataLoadTask.this.onComplete(syncComplete);
-+ } // else: cancelled
-+ return;
-+ }
-+
-+ this.schedule.data = newData.left;
-+ this.schedule.throwable = newData.right;
-+
-+ GenericDataLoadTask.this.processOnMain.queue();
-+ } else {
-+ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData);
-+ }
-+ }
-+ }
-+
-+ private final class ProcessOnMainTask implements Runnable {
-+
-+ private OnMain data;
-+ private Throwable throwable;
-+
-+ @Override
-+ public void run() {
-+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
-+ // cancelled
-+ return;
-+ }
-+ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable);
-+
-+ GenericDataLoadTask.this.onComplete(result);
-+ }
-+ }
-+
-+ protected static final class LoadDataFromDiskTask {
-+
-+ private volatile int priority;
-+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class);
-+
-+ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0;
-+ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1;
-+ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2;
-+
-+ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE;
-+
-+ private final int getPriorityVolatile() {
-+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this);
-+ }
-+
-+ private final int compareAndExchangePriorityVolatile(final int expect, final int update) {
-+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update);
-+ }
-+
-+ private final int getAndOrPriorityVolatile(final int val) {
-+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val);
-+ }
-+
-+ private final void setPriorityPlain(final int val) {
-+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val);
-+ }
-+
-+ private final ServerLevel world;
-+ private final int chunkX;
-+ private final int chunkZ;
-+
-+ private final MoonriseRegionFileIO.RegionFileType type;
-+ private Cancellable dataLoadTask;
-+ private Cancellable dataUnloadCancellable;
-+ private PrioritisedExecutor.PrioritisedTask dataUnloadTask;
-+
-+ private final BiConsumer<CompoundTag, Throwable> onComplete;
-+ private final AtomicBoolean scheduled = new AtomicBoolean();
-+
-+ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does
-+ // hold a priority lock.
-+ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final MoonriseRegionFileIO.RegionFileType type,
-+ final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.type = type;
-+ this.onComplete = onComplete;
-+ this.setPriorityPlain(priority.priority);
-+ }
-+
-+ private void complete(final CompoundTag data, final Throwable throwable) {
-+ try {
-+ this.onComplete.accept(data, throwable);
-+ } catch (final Throwable thr2) {
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
-+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable),
-+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type)
-+ ), thr2);
-+ }
-+ }
-+
-+ private boolean markExecuting() {
-+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
-+ }
-+
-+ private boolean isMarkedExecuted() {
-+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
-+ }
-+
-+ public void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.lowerPriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.setPriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
-+ throw new IllegalArgumentException("Invalid priority " + priority);
-+ }
-+
-+ int failures = 0;
-+ for (int curr = this.getPriorityVolatile();;) {
-+ if ((curr & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ return;
-+ }
-+
-+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.raisePriority(priority);
-+ }
-+ // no return - we need to propagate priority
-+ }
-+
-+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) {
-+ return;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
-+ return;
-+ }
-+
-+ // failed, retry
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+
-+ public void cancel() {
-+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed already
-+ return;
-+ }
-+
-+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
-+ // the write to dataLoadTask will check for the cancelled bit
-+ if (this.dataUnloadCancellable != null) {
-+ this.dataUnloadCancellable.cancel();
-+ }
-+
-+ if (this.dataLoadTask != null) {
-+ this.dataLoadTask.cancel();
-+ }
-+
-+ this.complete(CANCELLED_DATA, null);
-+ }
-+
-+ public void schedule() {
-+ if (this.scheduled.getAndSet(true)) {
-+ throw new IllegalStateException("schedule() called twice");
-+ }
-+ int priority = this.getPriorityVolatile();
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled
-+ return;
-+ }
-+
-+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
-+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
-+ // to mark complete here to ensure we do not double complete
-+ if (LoadDataFromDiskTask.this.markExecuting()) {
-+ LoadDataFromDiskTask.this.complete(data, thr);
-+ } // else: cancelled
-+ };
-+
-+ final Priority initialPriority = Priority.getPriority(priority);
-+ boolean scheduledUnload = false;
-+
-+ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
-+ if (holder != null) {
-+ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> {
-+ if (data != null) {
-+ consumer.accept(data, null);
-+ } else {
-+ // need to schedule task
-+ LoadDataFromDiskTask.this.schedule(false, consumer, Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
-+ }
-+ };
-+ Cancellable unloadCancellable = null;
-+ CompoundTag syncComplete = null;
-+ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists
-+ final CallbackCompletable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
-+ if (unloadCompletable != null) {
-+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
-+ if (unloadCancellable == null) {
-+ syncComplete = unloadCompletable.getResult();
-+ }
-+ }
-+
-+ if (syncComplete != null) {
-+ consumer.accept(syncComplete, null);
-+ return;
-+ }
-+
-+ if (unloadCancellable != null) {
-+ scheduledUnload = true;
-+ this.dataUnloadCancellable = unloadCancellable;
-+ this.dataUnloadTask = unloadTask.task();
-+ }
-+ }
-+
-+ this.schedule(scheduledUnload, consumer, initialPriority);
-+ }
-+
-+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final Priority initialPriority) {
-+ int priority = this.getPriorityVolatile();
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled
-+ return;
-+ }
-+
-+ if (!scheduledUnload) {
-+ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync(
-+ this.world, this.chunkX, this.chunkZ, this.type, consumer,
-+ initialPriority.isHigherPriority(Priority.NORMAL), initialPriority
-+ );
-+ }
-+
-+ int failures = 0;
-+ for (;;) {
-+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) {
-+ return;
-+ }
-+
-+ if ((priority & PRIORITY_EXECUTED) != 0) {
-+ // cancelled or executed
-+ if (this.dataUnloadCancellable != null) {
-+ this.dataUnloadCancellable.cancel();
-+ }
-+
-+ if (this.dataLoadTask != null) {
-+ this.dataLoadTask.cancel();
-+ }
-+ return;
-+ }
-+
-+ if (scheduledUnload) {
-+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ }
-+ } else {
-+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ }
-+
-+ ++failures;
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.server;
-+
-+public interface ChunkSystemMinecraftServer {
-+
-+ public void moonrise$setChunkSystemCrash(final Throwable throwable);
-+
-+ public void moonrise$executeMidTickTasks();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.status;
-+
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+
-+public interface ChunkSystemChunkStep {
-+
-+ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.storage;
-+
-+import net.minecraft.world.level.chunk.storage.RegionFile;
-+import java.io.IOException;
-+
-+public interface ChunkSystemChunkBuffer {
-+ public boolean moonrise$getWriteOnClose();
-+
-+ public void moonrise$setWriteOnClose(final boolean value);
-+
-+ public void moonrise$write(final RegionFile regionFile) throws IOException;
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.storage;
-+
-+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-+
-+public interface ChunkSystemChunkStorage {
-+
-+ public RegionFileStorage moonrise$getRegionStorage();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.storage;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.world.level.ChunkPos;
-+import java.io.IOException;
-+
-+public interface ChunkSystemRegionFile {
-+
-+ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final CompoundTag data, final ChunkPos pos) throws IOException;
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.ticket;
-+
-+public interface ChunkSystemTicket<T> {
-+
-+ public long moonrise$getRemoveDelay();
-+
-+ public void moonrise$setRemoveDelay(final long removeDelay);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.ticks;
-+
-+public interface ChunkSystemLevelChunkTicks {
-+
-+ public boolean moonrise$isDirty(final long tick);
-+
-+ public void moonrise$clearDirty();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
-@@ -0,0 +1,15 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.util;
-+
-+import net.minecraft.util.SortedArraySet;
-+
-+public interface ChunkSystemSortedArraySet<T> {
-+
-+ public SortedArraySet<T> moonrise$copy();
-+
-+ public Object[] moonrise$copyBackingArray();
-+
-+ public T moonrise$replace(final T object);
-+
-+ public T moonrise$removeAndGet(final T object);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-@@ -0,0 +1,321 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.util;
-+
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
-+import it.unimi.dsi.fastutil.HashCommon;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import java.util.Arrays;
-+import java.util.Objects;
-+
-+public final class ParallelSearchRadiusIteration {
-+
-+ // expected that this list returns for a given radius, the set of chunks ordered
-+ // by manhattan distance
-+ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[MoonriseConstants.MAX_VIEW_DISTANCE+2+1][];
-+ static {
-+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
-+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
-+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
-+ }
-+ }
-+
-+ public static long[] getSearchIteration(final int radius) {
-+ return SEARCH_RADIUS_ITERATION_LIST[radius];
-+ }
-+
-+ private static class CustomLongArray extends LongArrayList {
-+
-+ public CustomLongArray() {
-+ super();
-+ }
-+
-+ public CustomLongArray(final int expected) {
-+ super(expected);
-+ }
-+
-+ public boolean addAll(final CustomLongArray list) {
-+ this.addElements(this.size, list.a, 0, list.size);
-+ return list.size != 0;
-+ }
-+
-+ public void addUnchecked(final long value) {
-+ this.a[this.size++] = value;
-+ }
-+
-+ public void forceSize(final int to) {
-+ this.size = to;
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ long h = 1L;
-+
-+ Objects.checkFromToIndex(0, this.size, this.a.length);
-+
-+ for (int i = 0; i < this.size; ++i) {
-+ h = HashCommon.mix(h + this.a[i]);
-+ }
-+
-+ return (int)h;
-+ }
-+
-+ @Override
-+ public boolean equals(final Object o) {
-+ if (o == this) {
-+ return true;
-+ }
-+
-+ if (!(o instanceof CustomLongArray other)) {
-+ return false;
-+ }
-+
-+ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size);
-+ }
-+ }
-+
-+ private static int getDistanceSize(final int radius, final int max) {
-+ if (radius == 0) {
-+ return 1;
-+ }
-+ final int diff = radius - max;
-+ if (diff <= 0) {
-+ return 4*radius;
-+ }
-+ return 4*(max - Math.max(0, diff - 1));
-+ }
-+
-+ private static int getQ1DistanceSize(final int radius, final int max) {
-+ if (radius == 0) {
-+ return 1;
-+ }
-+ final int diff = radius - max;
-+ if (diff <= 0) {
-+ return radius+1;
-+ }
-+ return max - diff + 1;
-+ }
-+
-+ private static final class BasicFIFOLQueue {
-+
-+ private final long[] values;
-+ private int head, tail;
-+
-+ public BasicFIFOLQueue(final int cap) {
-+ if (cap <= 1) {
-+ throw new IllegalArgumentException();
-+ }
-+ this.values = new long[cap];
-+ }
-+
-+ public boolean isEmpty() {
-+ return this.head == this.tail;
-+ }
-+
-+ public long removeFirst() {
-+ final long ret = this.values[this.head];
-+
-+ if (this.head == this.tail) {
-+ throw new IllegalStateException();
-+ }
-+
-+ ++this.head;
-+ if (this.head == this.values.length) {
-+ this.head = 0;
-+ }
-+
-+ return ret;
-+ }
-+
-+ public void addLast(final long value) {
-+ this.values[this.tail++] = value;
-+
-+ if (this.tail == this.head) {
-+ throw new IllegalStateException();
-+ }
-+
-+ if (this.tail == this.values.length) {
-+ this.tail = 0;
-+ }
-+ }
-+ }
-+
-+ private static CustomLongArray[] makeQ1BFS(final int radius) {
-+ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1];
-+ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1);
-+ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1));
-+
-+ seen.add(CoordinateUtils.getChunkKey(0, 0));
-+ queue.addLast(CoordinateUtils.getChunkKey(0, 0));
-+ while (!queue.isEmpty()) {
-+ final long chunk = queue.removeFirst();
-+ final int chunkX = CoordinateUtils.getChunkX(chunk);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
-+
-+ final int index = Math.abs(chunkX) + Math.abs(chunkZ);
-+ final CustomLongArray list = ret[index];
-+ if (list != null) {
-+ list.addUnchecked(chunk);
-+ } else {
-+ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk);
-+ }
-+
-+ for (int i = 0; i < 4; ++i) {
-+ // 0 -> -1, 0
-+ // 1 -> 0, -1
-+ // 2 -> 1, 0
-+ // 3 -> 0, 1
-+
-+ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0)
-+ // note: -n = (~n) + 1
-+ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1)
-+
-+ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1
-+ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0
-+ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1
-+
-+ final int neighbourX = chunkX + dx;
-+ final int neighbourZ = chunkZ + dz;
-+ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
-+ // don't enqueue out of range
-+ continue;
-+ }
-+
-+ if (!seen.add(neighbour)) {
-+ continue;
-+ }
-+
-+ queue.addLast(neighbour);
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ // doesn't appear worth optimising this function now, even though it's 70% of the call
-+ private static CustomLongArray spread(final CustomLongArray input, final int size) {
-+ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input);
-+ final CustomLongArray added = new CustomLongArray(size);
-+
-+ while (!notAdded.isEmpty()) {
-+ if (added.isEmpty()) {
-+ added.addUnchecked(notAdded.removeLastLong());
-+ continue;
-+ }
-+
-+ long maxChunk = -1L;
-+ int maxDist = 0;
-+
-+ // select the chunk from the not yet added set that has the largest minimum distance from
-+ // the current set of added chunks
-+
-+ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) {
-+ final long chunkKey = iterator.nextLong();
-+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
-+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
-+
-+ int minDist = Integer.MAX_VALUE;
-+
-+ final int len = added.size();
-+ final long[] addedArr = added.elements();
-+ Objects.checkFromToIndex(0, len, addedArr.length);
-+ for (int i = 0; i < len; ++i) {
-+ final long addedKey = addedArr[i];
-+ final int addedX = CoordinateUtils.getChunkX(addedKey);
-+ final int addedZ = CoordinateUtils.getChunkZ(addedKey);
-+
-+ // here we use square distance because chunk generation uses neighbours in a square radius
-+ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ));
-+
-+ minDist = Math.min(dist, minDist);
-+ }
-+
-+ if (minDist > maxDist) {
-+ maxDist = minDist;
-+ maxChunk = chunkKey;
-+ }
-+ }
-+
-+ // move the selected chunk from the not added set to the added set
-+
-+ if (!notAdded.remove(maxChunk)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ added.addUnchecked(maxChunk);
-+ }
-+
-+ return added;
-+ }
-+
-+ private static void expandQuadrants(final CustomLongArray input, final int size) {
-+ final int len = input.size();
-+ final long[] array = input.elements();
-+
-+ int writeIndex = size - 1;
-+ for (int i = len - 1; i >= 0; --i) {
-+ final long key = array[i];
-+ final int chunkX = CoordinateUtils.getChunkX(key);
-+ final int chunkZ = CoordinateUtils.getChunkZ(key);
-+
-+ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ // Q4
-+ if (chunkZ != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ);
-+ }
-+ // Q3
-+ if (chunkX != 0 && chunkZ != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ);
-+ }
-+ // Q2
-+ if (chunkX != 0) {
-+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ);
-+ }
-+
-+ array[writeIndex--] = key;
-+ }
-+
-+ input.forceSize(size);
-+
-+ if (writeIndex != -1) {
-+ throw new IllegalStateException();
-+ }
-+ }
-+
-+ private static long[] generateBFSOrder(final int radius) {
-+ // by using only the first quadrant, we can reduce the total element size by 4 when spreading
-+ final CustomLongArray[] byDistance = makeQ1BFS(radius);
-+
-+ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating
-+ // this also means we are minimising locality
-+ // but, we need to maintain sorted order by manhatten distance
-+
-+ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other
-+ for (int i = 0, len = byDistance.length; i < len; ++i) {
-+ final CustomLongArray points = byDistance[i];
-+ final int expectedSize = getDistanceSize(i, radius);
-+
-+ final CustomLongArray spread = spread(points, expectedSize);
-+ // add in Q2, Q3, Q4
-+ expandQuadrants(spread, expectedSize);
-+
-+ byDistance[i] = spread;
-+ }
-+
-+ // now, rebuild the list so that it still maintains manhatten distance order
-+ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1));
-+
-+ for (final CustomLongArray dist : byDistance) {
-+ ret.addAll(dist);
-+ }
-+
-+ return ret.elements();
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
-@@ -0,0 +1,37 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.util.stream;
-+
-+import java.io.DataInputStream;
-+import java.io.FilterInputStream;
-+import java.io.InputStream;
-+import java.lang.reflect.Field;
-+
-+/**
-+ * Used to mark chunk data streams that are on external files
-+ */
-+public class ExternalChunkStreamMarker extends DataInputStream {
-+
-+ private static final Field IN_FIELD;
-+ static {
-+ Field field;
-+ try {
-+ field = FilterInputStream.class.getDeclaredField("in");
-+ field.setAccessible(true);
-+ } catch (final Throwable throwable) {
-+ field = null;
-+ }
-+
-+ IN_FIELD = field;
-+ }
-+
-+ private static InputStream getWrapped(final FilterInputStream in) {
-+ try {
-+ return (InputStream)IN_FIELD.get(in);
-+ } catch (final Throwable throwable) {
-+ return in;
-+ }
-+ }
-+
-+ public ExternalChunkStreamMarker(final DataInputStream in) {
-+ super(getWrapped(in));
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
-@@ -0,0 +1,12 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.world;
-+
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.phys.AABB;
-+import java.util.List;
-+import java.util.function.Predicate;
-+
-+public interface ChunkSystemEntityGetter {
-+
-+ public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
-@@ -0,0 +1,11 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.world;
-+
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+public interface ChunkSystemServerChunkCache {
-+
-+ public void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk);
-+
-+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e97e7d276faf055c89207385d3820debffb06463
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickConstants.java
-@@ -0,0 +1,7 @@
-+package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
-+
-+public final class ChunkTickConstants {
-+
-+ public static final int PLAYER_SPAWN_TRACK_RANGE = 8;
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a7253d32d8d0
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickDistanceManager.java
-@@ -0,0 +1,16 @@
-+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/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..6af03fd7807d4c71dbf85028d18dc850978ef429
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/chunk_tick_iteration/ChunkTickServerLevel.java
-@@ -0,0 +1,19 @@
-+package ca.spottedleaf.moonrise.patches.chunk_tick_iteration;
-+
-+import ca.spottedleaf.moonrise.common.list.ReferenceList;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+public interface ChunkTickServerLevel {
-+
-+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks();
-+
-+ public void moonrise$markChunkForPlayerTicking(final LevelChunk chunk);
-+
-+ public void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk);
-+
-+ public void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ);
-+
-+ public void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e04bd54744335fb5398c6e4f7ce8b981f35bfb7d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-@@ -0,0 +1,2183 @@
-+package ca.spottedleaf.moonrise.patches.collisions;
-+
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter;
-+import ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState;
-+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
-+import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData;
-+import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape;
-+import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape;
-+import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection;
-+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
-+import it.unimi.dsi.fastutil.doubles.DoubleList;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.vehicle.AbstractMinecart;
-+import net.minecraft.world.item.Item;
-+import net.minecraft.world.level.CollisionGetter;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.border.WorldBorder;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ChunkSource;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.PalettedContainer;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.material.FluidState;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
-+import net.minecraft.world.phys.shapes.ArrayVoxelShape;
-+import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
-+import net.minecraft.world.phys.shapes.BooleanOp;
-+import net.minecraft.world.phys.shapes.CollisionContext;
-+import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
-+import net.minecraft.world.phys.shapes.EntityCollisionContext;
-+import net.minecraft.world.phys.shapes.OffsetDoubleList;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.SliceShape;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Objects;
-+import java.util.function.BiPredicate;
-+import java.util.function.Predicate;
-+
-+public final class CollisionUtil {
-+
-+ public static final double COLLISION_EPSILON = 1.0E-7;
-+ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 });
-+
-+ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) {
-+ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON;
-+ }
-+
-+ public static boolean isEmpty(final AABB aabb) {
-+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) {
-+ double x = (double)(chunkX << 4);
-+ double z = (double)(chunkZ << 4);
-+ // use a bounding box bigger than the chunk to prevent entities from entering it on move
-+ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
-+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON));
-+ }
-+
-+ /*
-+ A couple of rules for VoxelShape collisions:
-+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
-+ checks.
-+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
-+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
-+ will automatically round it to 0.
-+ */
-+
-+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
-+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
-+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
-+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
-+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
-+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON &&
-+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON &&
-+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) {
-+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON &&
-+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON &&
-+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideX(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideY(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideZ(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // startIndex and endIndex inclusive
-+ // assumes indices are in range of array
-+ public static int findFloor(final double[] values, final double offset, final double value, int startIndex, int endIndex) {
-+ Objects.checkFromToIndex(startIndex, endIndex + 1, values.length);
-+ do {
-+ final int middle = (startIndex + endIndex) >>> 1;
-+ final double middleVal = (values[middle] + offset);
-+
-+ if (value < middleVal) {
-+ endIndex = middle - 1;
-+ } else {
-+ startIndex = middle + 1;
-+ }
-+ } while (startIndex <= endIndex);
-+
-+ return startIndex - 1;
-+ }
-+
-+ private static VoxelShape sliceShapeVanilla(final VoxelShape src, final Direction.Axis axis,
-+ final int index) {
-+ return new SliceShape(src, axis, index);
-+ }
-+
-+ private static DoubleList offsetList(final double[] src, final double by) {
-+ final DoubleArrayList wrap = DoubleArrayList.wrap(src);
-+ if (by == 0.0) {
-+ return wrap;
-+ }
-+ return new OffsetDoubleList(wrap, by);
-+ }
-+
-+ private static VoxelShape sliceShapeOptimised(final VoxelShape src, final Direction.Axis axis,
-+ final int index) {
-+ // assume index in range
-+ final double off_x = ((CollisionVoxelShape)src).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)src).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)src).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)src).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)src).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)src).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)src).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ final DoubleList list_x;
-+ final DoubleList list_y;
-+ final DoubleList list_z;
-+ final int shape_sx;
-+ final int shape_ex;
-+ final int shape_sy;
-+ final int shape_ey;
-+ final int shape_sz;
-+ final int shape_ez;
-+
-+ switch (axis) {
-+ case X: {
-+ // validate index
-+ if (index < 0 || index >= size_x) {
-+ return Shapes.empty();
-+ }
-+
-+ // test if input is already "sliced"
-+ if (coords_x.length == 2 && (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0) {
-+ return src;
-+ }
-+
-+ // test if result would be full box
-+ if (coords_y.length == 2 && coords_z.length == 2 &&
-+ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0 &&
-+ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
-+ // note: size_y == size_z == 1
-+ final int bitIdx = 0 + 0*size_z + index*(size_z*size_y);
-+ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
-+ }
-+
-+ list_x = ZERO_ONE;
-+ list_y = offsetList(coords_y, off_y);
-+ list_z = offsetList(coords_z, off_z);
-+ shape_sx = index;
-+ shape_ex = index + 1;
-+ shape_sy = 0;
-+ shape_ey = size_y;
-+ shape_sz = 0;
-+ shape_ez = size_z;
-+
-+ break;
-+ }
-+ case Y: {
-+ // validate index
-+ if (index < 0 || index >= size_y) {
-+ return Shapes.empty();
-+ }
-+
-+ // test if input is already "sliced"
-+ if (coords_y.length == 2 && (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) {
-+ return src;
-+ }
-+
-+ // test if result would be full box
-+ if (coords_x.length == 2 && coords_z.length == 2 &&
-+ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 &&
-+ (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
-+ // note: size_x == size_z == 1
-+ final int bitIdx = 0 + index*size_z + 0*(size_z*size_y);
-+ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
-+ }
-+
-+ list_x = offsetList(coords_x, off_x);
-+ list_y = ZERO_ONE;
-+ list_z = offsetList(coords_z, off_z);
-+ shape_sx = 0;
-+ shape_ex = size_x;
-+ shape_sy = index;
-+ shape_ey = index + 1;
-+ shape_sz = 0;
-+ shape_ez = size_z;
-+
-+ break;
-+ }
-+ case Z: {
-+ // validate index
-+ if (index < 0 || index >= size_z) {
-+ return Shapes.empty();
-+ }
-+
-+ // test if input is already "sliced"
-+ if (coords_z.length == 2 && (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
-+ return src;
-+ }
-+
-+ // test if result would be full box
-+ if (coords_x.length == 2 && coords_y.length == 2 &&
-+ (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 &&
-+ (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) {
-+ // note: size_x == size_y == 1
-+ final int bitIdx = index + 0*size_z + 0*(size_z*size_y);
-+ return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
-+ }
-+
-+ list_x = offsetList(coords_x, off_x);
-+ list_y = offsetList(coords_y, off_y);
-+ list_z = ZERO_ONE;
-+ shape_sx = 0;
-+ shape_ex = size_x;
-+ shape_sy = 0;
-+ shape_ey = size_y;
-+ shape_sz = index;
-+ shape_ez = index + 1;
-+
-+ break;
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis: " + axis);
-+ }
-+ }
-+
-+ final int local_len_x = shape_ex - shape_sx;
-+ final int local_len_y = shape_ey - shape_sy;
-+ final int local_len_z = shape_ez - shape_sz;
-+
-+ final BitSetDiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(local_len_x, local_len_y, local_len_z);
-+
-+ final int bitset_mul_x = size_z*size_y;
-+ final int idx_off = shape_sz + shape_sy*size_z + shape_sx*bitset_mul_x;
-+ final int shape_mul_x = local_len_y*local_len_z;
-+ for (int x = 0; x < local_len_x; ++x) {
-+ boolean setX = false;
-+ for (int y = 0; y < local_len_y; ++y) {
-+ boolean setY = false;
-+ for (int z = 0; z < local_len_z; ++z) {
-+ final int unslicedIdx = idx_off + z + y*size_z + x*bitset_mul_x;
-+ if ((bitset[unslicedIdx >>> 6] & (1L << unslicedIdx)) == 0L) {
-+ continue;
-+ }
-+
-+ setY = true;
-+ setX = true;
-+ shape.zMin = Math.min(shape.zMin, z);
-+ shape.zMax = Math.max(shape.zMax, z + 1);
-+
-+ shape.storage.set(
-+ z + y*local_len_z + x*shape_mul_x
-+ );
-+ }
-+
-+ if (setY) {
-+ shape.yMin = Math.min(shape.yMin, y);
-+ shape.yMax = Math.max(shape.yMax, y + 1);
-+ }
-+ }
-+ if (setX) {
-+ shape.xMin = Math.min(shape.xMin, x);
-+ shape.xMax = Math.max(shape.xMax, x + 1);
-+ }
-+ }
-+
-+ return shape.isEmpty() ? Shapes.empty() : new ArrayVoxelShape(
-+ shape, list_x, list_y, list_z
-+ );
-+ }
-+
-+ private static final boolean DEBUG_SLICE_SHAPE = false;
-+
-+ public static VoxelShape sliceShape(final VoxelShape src, final Direction.Axis axis,
-+ final int index) {
-+ final VoxelShape ret = sliceShapeOptimised(src, axis, index);
-+ if (DEBUG_SLICE_SHAPE) {
-+ final VoxelShape vanilla = sliceShapeVanilla(src, axis, index);
-+ if (!equals(ret, vanilla)) {
-+ // special case: SliceShape is not empty when it should be!
-+ if (areAnyFull(ret.shape) || areAnyFull(vanilla.shape)) {
-+ equals(ret, vanilla);
-+ sliceShapeOptimised(src, axis, index);
-+ throw new IllegalStateException("Slice shape mismatch");
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) {
-+ if (voxel.isEmpty()) {
-+ return false;
-+ }
-+
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = ((CollisionVoxelShape)voxel).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, off_x, aabb.minX + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, off_x, aabb.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, off_y, aabb.minY + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, off_y, aabb.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, off_z, aabb.minZ + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, off_z, aabb.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ // check bitset to check if any shapes in range are full
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return true;
-+ }
-+ }
-+ }
-+ }
-+
-+ return false;
-+ }
-+
-+ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideX(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideX(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, off_y, source.minY + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, off_y, source.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, off_z, source.minZ + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, off_z, source.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxX;
-+ final int ceil_max_x = findFloor(
-+ coords_x, off_x, source_max - COLLISION_EPSILON, 0, size_x
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) {
-+ double max_dist = (coords_x[curr_x] + off_x) - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minX;
-+ final int floor_min_x = findFloor(
-+ coords_x, off_x, source_min + COLLISION_EPSILON, 0, size_x
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) {
-+ double max_dist = (coords_x[curr_x + 1] + off_x) - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ public static double collideY(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideY(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, off_x, source.minX + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, off_x, source.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, off_z, source.minZ + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, off_z, source.maxZ - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxY;
-+ final int ceil_max_y = findFloor(
-+ coords_y, off_y, source_max - COLLISION_EPSILON, 0, size_y
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) {
-+ double max_dist = (coords_y[curr_y] + off_y) - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minY;
-+ final int floor_min_y = findFloor(
-+ coords_y, off_y, source_min + COLLISION_EPSILON, 0, size_y
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) {
-+ double max_dist = (coords_y[curr_y + 1] + off_y) - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = ((CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideZ(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = ((CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)target).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, off_x, source.minX + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, off_x, source.maxX - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, off_y, source.minY + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, off_y, source.maxY - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxZ;
-+ final int ceil_max_z = findFloor(
-+ coords_z, off_z, source_max - COLLISION_EPSILON, 0, size_z
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) {
-+ double max_dist = (coords_z[curr_z] + off_z) - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minZ;
-+ final int floor_min_z = findFloor(
-+ coords_z, off_z, source_min + COLLISION_EPSILON, 0, size_z
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) {
-+ double max_dist = (coords_z[curr_z + 1] + off_z) - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ // does not use epsilon
-+ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) {
-+ return strictlyContains(voxel, point.x, point.y, point.z);
-+ }
-+
-+ // does not use epsilon
-+ public static boolean strictlyContains(final VoxelShape voxel, final double x, final double y, final double z) {
-+ final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return single_aabb.contains(x, y, z);
-+ }
-+
-+ if (voxel.isEmpty()) {
-+ // bitset is clear, no point in searching
-+ return false;
-+ }
-+
-+ final double off_x = ((CollisionVoxelShape)voxel).moonrise$offsetX();
-+ final double off_y = ((CollisionVoxelShape)voxel).moonrise$offsetY();
-+ final double off_z = ((CollisionVoxelShape)voxel).moonrise$offsetZ();
-+
-+ final double[] coords_x = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max.
-+ // specifically, it cannot collide on the max bounds of the shape
-+
-+ final int index_x = findFloor(coords_x, off_x, x, 0, size_x);
-+ if (index_x < 0 || index_x >= size_x) {
-+ return false;
-+ }
-+
-+ final int index_y = findFloor(coords_y, off_y, y, 0, size_y);
-+ if (index_y < 0 || index_y >= size_y) {
-+ return false;
-+ }
-+
-+ final int index_z = findFloor(coords_z, off_z, z, 0, size_z);
-+ if (index_z < 0 || index_z >= size_z) {
-+ return false;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final int index = index_z + index_y*size_z + index_x*(size_z*size_y);
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ return (bitset[index >>> 6] & (1L << index)) != 0L;
-+ }
-+
-+ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) {
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3);
-+ }
-+
-+ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
-+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
-+ final MergedVoxelCoordinateList mergedZ,
-+ final int booleanOp) {
-+ final int sizeX = mergedX.voxels;
-+ final int sizeY = mergedY.voxels;
-+ final int sizeZ = mergedZ.voxels;
-+
-+ final long[] s1Voxels = shapeDataFirst.voxelSet();
-+ final long[] s2Voxels = shapeDataSecond.voxelSet();
-+
-+ final int s1Mul1 = shapeDataFirst.sizeZ();
-+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
-+
-+ final int s2Mul1 = shapeDataSecond.sizeZ();
-+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
-+
-+ // note: indices may contain -1, but nothing > size
-+ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
-+
-+ boolean empty = true;
-+
-+ int mergedIdx = 0;
-+ for (int idxX = 0; idxX < sizeX; ++idxX) {
-+ final int s1x = mergedX.firstIndices[idxX];
-+ final int s2x = mergedX.secondIndices[idxX];
-+ boolean setX = false;
-+ for (int idxY = 0; idxY < sizeY; ++idxY) {
-+ final int s1y = mergedY.firstIndices[idxY];
-+ final int s2y = mergedY.secondIndices[idxY];
-+ boolean setY = false;
-+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
-+ final int s1z = mergedZ.firstIndices[idxZ];
-+ final int s2z = mergedZ.secondIndices[idxZ];
-+
-+ int idx1;
-+ int idx2;
-+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L);
-+
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+
-+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
-+ setY |= res;
-+ setX |= res;
-+
-+ if (res) {
-+ empty = false;
-+ // inline and optimize fill operation
-+ ret.zMin = Math.min(ret.zMin, idxZ);
-+ ret.zMax = Math.max(ret.zMax, idxZ + 1);
-+ ret.storage.set(mergedIdx);
-+ }
-+
-+ ++mergedIdx;
-+ }
-+ if (setY) {
-+ ret.yMin = Math.min(ret.yMin, idxY);
-+ ret.yMax = Math.max(ret.yMax, idxY + 1);
-+ }
-+ }
-+ if (setX) {
-+ ret.xMin = Math.min(ret.xMin, idxX);
-+ ret.xMax = Math.max(ret.xMax, idxX + 1);
-+ }
-+ }
-+
-+ return empty ? null : ret;
-+ }
-+
-+ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
-+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
-+ final MergedVoxelCoordinateList mergedZ,
-+ final int booleanOp) {
-+ final int sizeX = mergedX.voxels;
-+ final int sizeY = mergedY.voxels;
-+ final int sizeZ = mergedZ.voxels;
-+
-+ final long[] s1Voxels = shapeDataFirst.voxelSet();
-+ final long[] s2Voxels = shapeDataSecond.voxelSet();
-+
-+ final int s1Mul1 = shapeDataFirst.sizeZ();
-+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
-+
-+ final int s2Mul1 = shapeDataSecond.sizeZ();
-+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
-+
-+ // note: indices may contain -1, but nothing > size
-+ for (int idxX = 0; idxX < sizeX; ++idxX) {
-+ final int s1x = mergedX.firstIndices[idxX];
-+ final int s2x = mergedX.secondIndices[idxX];
-+ for (int idxY = 0; idxY < sizeY; ++idxY) {
-+ final int s1y = mergedY.firstIndices[idxY];
-+ final int s2y = mergedY.secondIndices[idxY];
-+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
-+ final int s1z = mergedZ.firstIndices[idxZ];
-+ final int s2z = mergedZ.secondIndices[idxZ];
-+
-+ int idx1;
-+ int idx2;
-+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx1 = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx1) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx2 = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx2) & 1L);
-+
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+
-+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
-+
-+ if (res) {
-+ return false;
-+ }
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ return joinUnoptimized(first, second, operator).optimize();
-+ }
-+
-+ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ final boolean ff = operator.apply(false, false);
-+ if (ff) {
-+ // technically, should be an infinite box but that's clearly an error
-+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
-+ }
-+
-+ final boolean tt = operator.apply(true, true);
-+
-+ if (first == second) {
-+ return tt ? first : Shapes.empty();
-+ }
-+
-+ final boolean ft = operator.apply(false, true);
-+ final boolean tf = operator.apply(true, false);
-+
-+ if (first.isEmpty()) {
-+ return ft ? second : Shapes.empty();
-+ }
-+ if (second.isEmpty()) {
-+ return tf ? first : Shapes.empty();
-+ }
-+
-+ if (!tt) {
-+ // try to check for no intersection, since tt = false
-+ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-+
-+ final boolean intersect;
-+
-+ final boolean hasAABBF = aabbF != null;
-+ final boolean hasAABBS = aabbS != null;
-+ if (hasAABBF | hasAABBS) {
-+ if (hasAABBF & hasAABBS) {
-+ intersect = voxelShapeIntersect(aabbF, aabbS);
-+ } else if (hasAABBF) {
-+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
-+ } else {
-+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
-+ }
-+ } else {
-+ // expect cached bounds
-+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
-+ }
-+
-+ if (!intersect) {
-+ if (!tf & !ft) {
-+ return Shapes.empty();
-+ }
-+ if (!tf | !ft) {
-+ return tf ? first : second;
-+ }
-+ }
-+ }
-+
-+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(),
-+ ft, tf
-+ );
-+ if (mergedX == null) {
-+ return Shapes.empty();
-+ }
-+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(),
-+ ft, tf
-+ );
-+ if (mergedY == null) {
-+ return Shapes.empty();
-+ }
-+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(),
-+ ft, tf
-+ );
-+ if (mergedZ == null) {
-+ return Shapes.empty();
-+ }
-+
-+ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-+
-+ final BitSetDiscreteVoxelShape mergedShape = merge(
-+ shapeDataFirst, shapeDataSecond,
-+ mergedX, mergedY, mergedZ,
-+ makeBitset(ft, tf, tt)
-+ );
-+
-+ if (mergedShape == null) {
-+ return Shapes.empty();
-+ }
-+
-+ return new ArrayVoxelShape(
-+ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords()
-+ );
-+ }
-+
-+ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ final boolean ff = operator.apply(false, false);
-+ if (ff) {
-+ // technically, should be an infinite box but that's clearly an error
-+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
-+ }
-+ final boolean firstEmpty = first.isEmpty();
-+ final boolean secondEmpty = second.isEmpty();
-+ if (firstEmpty | secondEmpty) {
-+ return operator.apply(!firstEmpty, !secondEmpty);
-+ }
-+
-+ final boolean tt = operator.apply(true, true);
-+
-+ if (first == second) {
-+ return tt;
-+ }
-+
-+ final boolean ft = operator.apply(false, true);
-+ final boolean tf = operator.apply(true, false);
-+
-+ // try to check intersection
-+ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-+
-+ final boolean intersect;
-+
-+ final boolean hasAABBF = aabbF != null;
-+ final boolean hasAABBS = aabbS != null;
-+ if (hasAABBF | hasAABBS) {
-+ if (hasAABBF & hasAABBS) {
-+ intersect = voxelShapeIntersect(aabbF, aabbS);
-+ } else if (hasAABBF) {
-+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
-+ } else {
-+ // hasAABBS -> true
-+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
-+ }
-+
-+ if (!intersect) {
-+ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty
-+ return tf | ft;
-+ } else if (tt) {
-+ // intersect = true && tt = true -> non-empty merged shape
-+ return true;
-+ }
-+ } else {
-+ // expect cached bounds
-+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
-+ if (!intersect) {
-+ // is only non-empty if we take from first or second, as there is no intersection
-+ return tf | ft;
-+ }
-+ }
-+
-+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)first).moonrise$offsetX(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((CollisionVoxelShape)second).moonrise$offsetX(),
-+ ft, tf
-+ );
-+ if (mergedX == null) {
-+ return false;
-+ }
-+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)first).moonrise$offsetY(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((CollisionVoxelShape)second).moonrise$offsetY(),
-+ ft, tf
-+ );
-+ if (mergedY == null) {
-+ return false;
-+ }
-+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
-+ ((CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)first).moonrise$offsetZ(),
-+ ((CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((CollisionVoxelShape)second).moonrise$offsetZ(),
-+ ft, tf
-+ );
-+ if (mergedZ == null) {
-+ return false;
-+ }
-+
-+ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-+
-+ return !isMergeEmpty(
-+ shapeDataFirst, shapeDataSecond,
-+ mergedX, mergedY, mergedZ,
-+ makeBitset(ft, tf, tt)
-+ );
-+ }
-+
-+ private static final class MergedVoxelCoordinateList {
-+
-+ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][];
-+ static {
-+ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) {
-+ SIMPLE_INDICES_CACHE[i] = getIndices(i);
-+ }
-+ }
-+
-+ private static int[] getIndices(final int length) {
-+ final int[] ret = new int[length];
-+
-+ for (int i = 1; i < length; ++i) {
-+ ret[i] = i;
-+ }
-+
-+ return ret;
-+ }
-+
-+ // indices above voxel size are always set to -1
-+ public final double[] coordinates;
-+ public final double coordinateOffset;
-+ public final int[] firstIndices;
-+ public final int[] secondIndices;
-+ public final int voxels;
-+
-+ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset,
-+ final int[] firstIndices, final int[] secondIndices, final int voxels) {
-+ this.coordinates = coordinates;
-+ this.coordinateOffset = coordinateOffset;
-+ this.firstIndices = firstIndices;
-+ this.secondIndices = secondIndices;
-+ this.voxels = voxels;
-+ }
-+
-+ public DoubleList wrapCoords() {
-+ if (this.coordinateOffset == 0.0) {
-+ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
-+ }
-+ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
-+ }
-+
-+ // assume coordinates.length > 1
-+ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) {
-+ final int voxels = coordinates.length - 1;
-+ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels);
-+
-+ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
-+ }
-+
-+ // assume coordinates.length > 1
-+ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset,
-+ final double[] secondCoordinates, final double secondOffset,
-+ final boolean ft, final boolean tf) {
-+ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) {
-+ return getForSingle(firstCoordinates, firstOffset);
-+ }
-+
-+ final int firstCount = firstCoordinates.length;
-+ final int secondCount = secondCoordinates.length;
-+
-+ final int voxelsFirst = firstCount - 1;
-+ final int voxelsSecond = secondCount - 1;
-+
-+ final int maxCount = firstCount + secondCount;
-+
-+ final double[] coordinates = new double[maxCount];
-+ final int[] firstIndices = new int[maxCount];
-+ final int[] secondIndices = new int[maxCount];
-+
-+ final boolean notTF = !tf;
-+ final boolean notFT = !ft;
-+
-+ int firstIndex = 0;
-+ int secondIndex = 0;
-+ int resultSize = 0;
-+
-+ // note: operations on NaN are false
-+ double last = Double.NaN;
-+
-+ for (;;) {
-+ final boolean noneLeftFirst = firstIndex >= firstCount;
-+ final boolean noneLeftSecond = secondIndex >= secondCount;
-+
-+ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) {
-+ break;
-+ }
-+
-+ final boolean firstZero = firstIndex == 0;
-+ final boolean secondZero = secondIndex == 0;
-+
-+ final double select;
-+
-+ if (noneLeftFirst) {
-+ // noneLeftSecond -> false
-+ // notFT -> false
-+ select = secondCoordinates[secondIndex] + secondOffset;
-+ ++secondIndex;
-+ } else if (noneLeftSecond) {
-+ // noneLeftFirst -> false
-+ // notTF -> false
-+ select = firstCoordinates[firstIndex] + firstOffset;
-+ ++firstIndex;
-+ } else {
-+ // noneLeftFirst | noneLeftSecond -> false
-+ // notTF -> ??
-+ // notFT -> ??
-+ final boolean breakFirst = notTF & secondZero;
-+ final boolean breakSecond = notFT & firstZero;
-+
-+ final double first = firstCoordinates[firstIndex] + firstOffset;
-+ final double second = secondCoordinates[secondIndex] + secondOffset;
-+ final boolean useFirst = first < (second + COLLISION_EPSILON);
-+ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond);
-+
-+ select = useFirst ? first : second;
-+ firstIndex += useFirst ? 1 : 0;
-+ secondIndex += 1 ^ (useFirst ? 1 : 0);
-+
-+ if (cont) {
-+ continue;
-+ }
-+ }
-+
-+ int prevFirst = firstIndex - 1;
-+ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst;
-+ int prevSecond = secondIndex - 1;
-+ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond;
-+
-+ if (last >= (select - COLLISION_EPSILON)) {
-+ // note: any operations on NaN is false
-+ firstIndices[resultSize - 1] = prevFirst;
-+ secondIndices[resultSize - 1] = prevSecond;
-+ } else {
-+ firstIndices[resultSize] = prevFirst;
-+ secondIndices[resultSize] = prevSecond;
-+ coordinates[resultSize] = select;
-+
-+ ++resultSize;
-+ last = select;
-+ }
-+ }
-+
-+ return resultSize <= 1 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
-+ }
-+ }
-+
-+ public static boolean equals(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) {
-+ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData();
-+ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData();
-+
-+ final boolean isEmpty1 = cachedShapeData1.isEmpty();
-+ final boolean isEmpty2 = cachedShapeData2.isEmpty();
-+
-+ if (isEmpty1 & isEmpty2) {
-+ return true;
-+ } else if (isEmpty1 ^ isEmpty2) {
-+ return false;
-+ } // else: isEmpty1 = isEmpty2 = false
-+
-+ if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
-+ return false;
-+ }
-+
-+ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) {
-+ return false;
-+ }
-+ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) {
-+ return false;
-+ }
-+ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) {
-+ return false;
-+ }
-+
-+ return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet());
-+ }
-+
-+ // useful only for testing
-+ public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) {
-+ if (shape1.isEmpty() & shape2.isEmpty()) {
-+ return true;
-+ } else if (shape1.isEmpty() ^ shape2.isEmpty()) {
-+ return false;
-+ }
-+
-+ if (!equals(shape1.shape, shape2.shape)) {
-+ return false;
-+ }
-+
-+ return shape1.getCoords(Direction.Axis.X).equals(shape2.getCoords(Direction.Axis.X)) &&
-+ shape1.getCoords(Direction.Axis.Y).equals(shape2.getCoords(Direction.Axis.Y)) &&
-+ shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z));
-+ }
-+
-+ public static boolean areAnyFull(final DiscreteVoxelShape shape) {
-+ if (shape.isEmpty()) {
-+ return false;
-+ }
-+
-+ final int sizeX = shape.getXSize();
-+ final int sizeY = shape.getYSize();
-+ final int sizeZ = shape.getZSize();
-+
-+ for (int x = 0; x < sizeX; ++x) {
-+ for (int y = 0; y < sizeY; ++y) {
-+ for (int z = 0; z < sizeZ; ++z) {
-+ if (shape.isFull(x, y, z)) {
-+ return true;
-+ }
-+ }
-+ }
-+ }
-+
-+ return false;
-+ }
-+
-+ public static String shapeMismatch(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) {
-+ final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData();
-+ final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData();
-+
-+ final boolean isEmpty1 = cachedShapeData1.isEmpty();
-+ final boolean isEmpty2 = cachedShapeData2.isEmpty();
-+
-+ if (isEmpty1 & isEmpty2) {
-+ return null;
-+ } else if (isEmpty1 ^ isEmpty2) {
-+ return null;
-+ } // else: isEmpty1 = isEmpty2 = false
-+
-+ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) {
-+ return "size x: " + cachedShapeData1.sizeX() + " != " + cachedShapeData2.sizeX();
-+ }
-+ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) {
-+ return "size y: " + cachedShapeData1.sizeY() + " != " + cachedShapeData2.sizeY();
-+ }
-+ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) {
-+ return "size z: " + cachedShapeData1.sizeZ() + " != " + cachedShapeData2.sizeZ();
-+ }
-+
-+ final StringBuilder ret = new StringBuilder();
-+
-+ final int sizeX = cachedShapeData1.sizeX();;
-+ final int sizeY = cachedShapeData1.sizeY();
-+ final int sizeZ = cachedShapeData1.sizeZ();
-+
-+ boolean first = true;
-+
-+ for (int x = 0; x < sizeX; ++x) {
-+ for (int y = 0; y < sizeY; ++y) {
-+ for (int z = 0; z < sizeZ; ++z) {
-+ final boolean isFull1 = shape1.isFull(x, y, z);
-+ final boolean isFull2 = shape2.isFull(x, y, z);
-+
-+ if (isFull1 == isFull2) {
-+ continue;
-+ }
-+
-+ if (first) {
-+ first = false;
-+ } else {
-+ ret.append(", ");
-+ }
-+
-+ ret.append("(").append(x).append(",").append(y).append(",").append(z)
-+ .append("): shape1: ").append(isFull1).append(", shape2: ").append(isFull2);
-+ }
-+ }
-+ }
-+
-+ return ret.isEmpty() ? null : ret.toString();
-+ }
-+
-+ public static AABB offsetX(final AABB box, final double dx) {
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB offsetY(final AABB box, final double dy) {
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ);
-+ }
-+
-+ public static AABB offsetZ(final AABB box, final double dz) {
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz);
-+ }
-+
-+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ);
-+ }
-+
-+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz);
-+ }
-+
-+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ);
-+ }
-+
-+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ);
-+ }
-+
-+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz);
-+ }
-+
-+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ);
-+ }
-+
-+ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideX(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideY(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideZ(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideX(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideY(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideZ(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<VoxelShape> potentialCollisions) {
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) {
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb,
-+ final List<VoxelShape> voxels,
-+ final List<AABB> aabbs) {
-+ if (voxels.isEmpty()) {
-+ // fast track only AABBs
-+ return performAABBCollisions(moveVector, axisalignedbb, aabbs);
-+ }
-+
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performAABBCollisionsY(axisalignedbb, y, aabbs);
-+ y = performVoxelCollisionsY(axisalignedbb, y, voxels);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
-+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performAABBCollisionsX(axisalignedbb, x, aabbs);
-+ x = performVoxelCollisionsX(axisalignedbb, x, voxels);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
-+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) {
-+ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
-+ }
-+
-+ public static boolean isCollidingWithBorder(final WorldBorder worldborder,
-+ final double boxMinX, final double boxMaxX,
-+ final double boxMinZ, final double boxMaxZ) {
-+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X
-+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X
-+
-+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z
-+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z
-+
-+ // inverted check for world border enclosing the specified box expanded by -EPSILON
-+ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON ||
-+ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON;
-+ }
-+
-+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */
-+ private static double min(final double x, final double y) {
-+ return x < y ? x : y;
-+ }
-+
-+ private static double max(final double x, final double y) {
-+ return x > y ? x : y;
-+ }
-+
-+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0;
-+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1;
-+ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2;
-+ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3;
-+
-+ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb,
-+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB,
-+ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) {
-+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
-+ boolean ret = false;
-+
-+ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) {
-+ final WorldBorder worldBorder = world.getWorldBorder();
-+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ final VoxelShape borderShape = worldBorder.getCollisionShape();
-+ intoVoxel.add(borderShape);
-+ ret = true;
-+ }
-+ }
-+ }
-+
-+ final int minSection = WorldUtil.getMinSection(world);
-+
-+ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-+ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
-+
-+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1);
-+ final int maxBlockY = Math.min((WorldUtil.getMaxSection(world) << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1);
-+
-+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
-+
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity);
-+ final boolean useEntityCollisionShape = LazyEntityCollisionContext.useEntityCollisionShape(world, entity);
-+
-+ // special cases:
-+ if (minBlockY > maxBlockY) {
-+ // no point in checking
-+ return ret;
-+ }
-+
-+ final int minChunkX = minBlockX >> 4;
-+ final int maxChunkX = maxBlockX >> 4;
-+
-+ final int minChunkY = minBlockY >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+
-+ final int minChunkZ = minBlockZ >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-+
-+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
-+ final ChunkSource chunkSource = world.getChunkSource();
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
-+
-+ if (chunk == null) {
-+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
-+ ret = true;
-+ }
-+ }
-+ continue;
-+ }
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ // bound y
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
-+ continue;
-+ }
-+ final LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
-+ continue;
-+ }
-+
-+ final boolean hasSpecial = ((BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks();
-+ final int sectionAdjust = !hasSpecial ? 1 : 0;
-+
-+ final PalettedContainer<BlockState> blocks = section.states;
-+
-+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
-+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
-+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0;
-+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15;
-+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0;
-+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15;
-+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ final int blockY = currY | (currChunkY << 4);
-+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
-+ final int blockZ = currZ | (currChunkZ << 4);
-+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
-+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
-+ final int blockX = currX | (currChunkX << 4);
-+
-+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
-+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
-+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0;
-+ if (edgeCount == 3) {
-+ continue;
-+ }
-+
-+ final BlockState blockData = blocks.get(localBlockIndex);
-+
-+ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) {
-+ continue;
-+ }
-+
-+ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape();
-+
-+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
-+ if (useEntityCollisionShape) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ blockCollision = collisionShape.getCollisionShape(blockData, world, mutablePos);
-+ } else if (blockCollision == null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape);
-+ }
-+
-+ AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
-+ if (!voxelShapeIntersect(aabb, singleAABB)) {
-+ continue;
-+ }
-+
-+ if (predicate != null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (!predicate.test(blockData, mutablePos)) {
-+ continue;
-+ }
-+ }
-+
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ ret = true;
-+ intoAABB.add(singleAABB);
-+ continue;
-+ }
-+ }
-+
-+ if (blockCollision.isEmpty()) {
-+ continue;
-+ }
-+
-+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
-+
-+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
-+ continue;
-+ }
-+
-+ if (predicate != null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (!predicate.test(blockData, mutablePos)) {
-+ continue;
-+ }
-+ }
-+
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ ret = true;
-+ intoVoxel.add(blockCollisionOffset);
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getEntityHardCollisions(final Level world, final Entity entity, AABB aabb,
-+ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) {
-+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
-+
-+ boolean ret = false;
-+
-+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
-+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
-+ // specifically with boat collisions.
-+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON);
-+ final List<Entity> entities;
-+ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+ entities = world.getEntities(entity, aabb, predicate);
-+ } else {
-+ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate);
-+ }
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator()) {
-+ continue;
-+ }
-+
-+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ into.add(otherEntity.getBoundingBox());
-+ ret = true;
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb,
-+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags,
-+ final BiPredicate<BlockState, BlockPos> blockPredicate,
-+ final Predicate<Entity> entityPredicate) {
-+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) {
-+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
-+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
-+ } else {
-+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
-+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
-+ }
-+ }
-+
-+ public static final class LazyEntityCollisionContext extends EntityCollisionContext {
-+
-+ private CollisionContext delegate;
-+ private boolean delegated;
-+
-+ public LazyEntityCollisionContext(final Entity entity) {
-+ super(false, 0.0, null, null, entity);
-+ }
-+
-+ public static boolean useEntityCollisionShape(final Level world, final Entity entity) {
-+ return entity instanceof AbstractMinecart && AbstractMinecart.useExperimentalMovement(world);
-+ }
-+
-+ public boolean isDelegated() {
-+ final boolean delegated = this.delegated;
-+ this.delegated = false;
-+ return delegated;
-+ }
-+
-+ public CollisionContext getDelegate() {
-+ this.delegated = true;
-+ final Entity entity = super.getEntity();
-+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate;
-+ }
-+
-+ @Override
-+ public Entity getEntity() {
-+ this.getDelegate();
-+ return super.getEntity();
-+ }
-+
-+ @Override
-+ public boolean isDescending() {
-+ return this.getDelegate().isDescending();
-+ }
-+
-+ @Override
-+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) {
-+ return this.getDelegate().isAbove(shape, pos, defaultValue);
-+ }
-+
-+ @Override
-+ public boolean isHoldingItem(final Item item) {
-+ return this.getDelegate().isHoldingItem(item);
-+ }
-+
-+ @Override
-+ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) {
-+ return this.getDelegate().canStandOnFluid(state, fluidState);
-+ }
-+
-+ @Override
-+ public VoxelShape getCollisionShape(final BlockState blockState, final CollisionGetter collisionGetter, final BlockPos blockPos) {
-+ return this.getDelegate().getCollisionShape(blockState, collisionGetter, blockPos);
-+ }
-+ }
-+
-+ private CollisionUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-@@ -0,0 +1,28 @@
-+package ca.spottedleaf.moonrise.patches.collisions;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.material.FluidState;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+public final class ExplosionBlockCache {
-+
-+ public final long key;
-+ public final BlockPos immutablePos;
-+ public final BlockState blockState;
-+ public final FluidState fluidState;
-+ public final float resistance;
-+ public final boolean outOfWorld;
-+ public Boolean shouldExplode; // null -> not called yet
-+ public VoxelShape cachedCollisionShape;
-+
-+ public ExplosionBlockCache(final long key, final BlockPos immutablePos, final BlockState blockState,
-+ final FluidState fluidState, final float resistance, final boolean outOfWorld) {
-+ this.key = key;
-+ this.immutablePos = immutablePos;
-+ this.blockState = blockState;
-+ this.fluidState = fluidState;
-+ this.resistance = resistance;
-+ this.outOfWorld = outOfWorld;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-@@ -0,0 +1,29 @@
-+package ca.spottedleaf.moonrise.patches.collisions.block;
-+
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+public interface CollisionBlockState {
-+
-+ // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache())
-+ // and whether Shapes.faceShapeOccludes(EMPTY, cached shape) is true
-+ public boolean moonrise$occludesFullBlock();
-+
-+ // whether the cached collision shape exists and is empty
-+ public boolean moonrise$emptyCollisionShape();
-+
-+ // whether the context-sensitive shape is constant and is empty
-+ public boolean moonrise$emptyContextCollisionShape();
-+
-+ // indicates that occludesFullBlock is cached for the collision shape
-+ public boolean moonrise$hasCache();
-+
-+ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned
-+ // value is still unique
-+ public int moonrise$uniqueId1();
-+
-+ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned
-+ // value is still unique
-+ public int moonrise$uniqueId2();
-+
-+ public VoxelShape moonrise$getConstantContextCollisionShape();
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
-@@ -0,0 +1,10 @@
-+package ca.spottedleaf.moonrise.patches.collisions.shape;
-+
-+public record CachedShapeData(
-+ int sizeX, int sizeY, int sizeZ,
-+ long[] voxelSet,
-+ int minFullX, int minFullY, int minFullZ,
-+ int maxFullX, int maxFullY, int maxFullZ,
-+ boolean isEmpty, boolean hasSingleAABB
-+) {
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-@@ -0,0 +1,39 @@
-+package ca.spottedleaf.moonrise.patches.collisions.shape;
-+
-+import net.minecraft.world.phys.AABB;
-+import java.util.ArrayList;
-+import java.util.List;
-+
-+public record CachedToAABBs(
-+ List<AABB> aabbs,
-+ boolean isOffset,
-+ double offX, double offY, double offZ
-+) {
-+
-+ public CachedToAABBs removeOffset() {
-+ final List<AABB> toOffset = this.aabbs;
-+ final double offX = this.offX;
-+ final double offY = this.offY;
-+ final double offZ = this.offZ;
-+
-+ final List<AABB> ret = new ArrayList<>(toOffset.size());
-+
-+ for (int i = 0, len = toOffset.size(); i < len; ++i) {
-+ ret.add(toOffset.get(i).move(offX, offY, offZ));
-+ }
-+
-+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
-+ }
-+
-+ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) {
-+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) {
-+ return cache;
-+ }
-+
-+ final double resX = cache.offX + offX;
-+ final double resY = cache.offY + offY;
-+ final double resZ = cache.offZ + offZ;
-+
-+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-@@ -0,0 +1,7 @@
-+package ca.spottedleaf.moonrise.patches.collisions.shape;
-+
-+public interface CollisionDiscreteVoxelShape {
-+
-+ public CachedShapeData moonrise$getOrCreateCachedShapeData();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-@@ -0,0 +1,40 @@
-+package ca.spottedleaf.moonrise.patches.collisions.shape;
-+
-+import net.minecraft.core.Direction;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+public interface CollisionVoxelShape {
-+
-+ public double moonrise$offsetX();
-+
-+ public double moonrise$offsetY();
-+
-+ public double moonrise$offsetZ();
-+
-+ public double[] moonrise$rootCoordinatesX();
-+
-+ public double[] moonrise$rootCoordinatesY();
-+
-+ public double[] moonrise$rootCoordinatesZ();
-+
-+ public CachedShapeData moonrise$getCachedVoxelData();
-+
-+ // rets null if not possible to represent this shape as one AABB
-+ public AABB moonrise$getSingleAABBRepresentation();
-+
-+ // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC
-+ public void moonrise$initCache();
-+
-+ // this returns empty if not clamped to 1.0 or 0.0 depending on direction
-+ public VoxelShape moonrise$getFaceShapeClamped(final Direction direction);
-+
-+ public boolean moonrise$isFullBlock();
-+
-+ public boolean moonrise$occludesFullBlock();
-+
-+ public boolean moonrise$occludesFullBlockIfCached();
-+
-+ // uses a cache internally
-+ public VoxelShape moonrise$orUnoptimized(final VoxelShape other);
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-@@ -0,0 +1,10 @@
-+package ca.spottedleaf.moonrise.patches.collisions.shape;
-+
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+public record MergedORCache(
-+ VoxelShape key,
-+ VoxelShape result
-+) {
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.collisions.util;
-+
-+public interface CollisionDirection {
-+
-+ // note: this is HashCommon#murmurHash3(some unique id) and since murmurHash3 has an inverse function the returned
-+ // value is still unique
-+ public int moonrise$uniqueId();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-@@ -0,0 +1,7 @@
-+package ca.spottedleaf.moonrise.patches.collisions.util;
-+
-+import net.minecraft.core.Direction;
-+import net.minecraft.world.level.block.state.BlockState;
-+
-+public record FluidOcclusionCacheKey(BlockState first, BlockState second, Direction direction, boolean result) {
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
-@@ -0,0 +1,11 @@
-+package ca.spottedleaf.moonrise.patches.entity_tracker;
-+
-+import net.minecraft.server.level.ChunkMap;
-+
-+public interface EntityTrackerEntity {
-+
-+ public ChunkMap.TrackedEntity moonrise$getTrackedEntity();
-+
-+ public void moonrise$setTrackedEntity(final ChunkMap.TrackedEntity trackedEntity);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-@@ -0,0 +1,15 @@
-+package ca.spottedleaf.moonrise.patches.entity_tracker;
-+
-+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
-+
-+public interface EntityTrackerTrackedEntity {
-+
-+ public void moonrise$tick(final NearbyPlayers.TrackedChunk chunk);
-+
-+ public void moonrise$removeNonTickThreadPlayers();
-+
-+ public void moonrise$clearPlayers();
-+
-+ public boolean moonrise$hasPlayers();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.fast_palette;
-+
-+public interface FastPalette<T> {
-+
-+ public default T[] moonrise$getRawPalette(final FastPaletteData<T> src) {
-+ return null;
-+ }
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.fast_palette;
-+
-+public interface FastPaletteData<T> {
-+
-+ public T[] moonrise$getPalette();
-+
-+ public void moonrise$setPalette(final T[] palette);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
-@@ -0,0 +1,5 @@
-+package ca.spottedleaf.moonrise.patches.fluid;
-+
-+public interface FluidFluidState {
-+ public void moonrise$initCaches();
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.getblock;
-+
-+import net.minecraft.world.level.block.state.BlockState;
-+
-+public interface GetBlockChunk {
-+
-+ public BlockState moonrise$getBlock(final int x, final int y, final int z);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-@@ -0,0 +1,7 @@
-+package ca.spottedleaf.moonrise.patches.starlight.blockstate;
-+
-+public interface StarlightAbstractBlockState {
-+
-+ public boolean starlight$isConditionallyFullOpaque();
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
-@@ -0,0 +1,18 @@
-+package ca.spottedleaf.moonrise.patches.starlight.chunk;
-+
-+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
-+
-+public interface StarlightChunk {
-+
-+ public SWMRNibbleArray[] starlight$getBlockNibbles();
-+ public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles);
-+
-+ public SWMRNibbleArray[] starlight$getSkyNibbles();
-+ public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles);
-+
-+ public boolean[] starlight$getSkyEmptinessMap();
-+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap);
-+
-+ public boolean[] starlight$getBlockEmptinessMap();
-+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap);
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-@@ -0,0 +1,280 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
-+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.chunk.PalettedContainer;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Set;
-+
-+public final class BlockStarLightEngine extends StarLightEngine {
-+
-+ public BlockStarLightEngine(final Level world) {
-+ super(false, world);
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return ((StarlightChunk)chunk).starlight$getBlockNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ ((StarlightChunk)chunk).starlight$setBlockNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
-+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
-+ // in increases, and thus the existing sky block check will actually correctly propagate light through
-+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
-+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
-+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
-+ // of vanilla data management we "hide" them.
-+ nibble.setHidden();
-+ }
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
-+ }
-+ } else {
-+ nibble.setNonNull();
-+ }
-+ }
-+
-+ @Override
-+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change emitted light
-+ // blocks can change direction of propagation
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-+ final int emittedLevel = (PlatformHooks.get().getLightEmission(blockState, lightAccess.getLevel(), this.lightEmissionPos.set(worldX, worldY, worldZ))) & emittedMask;
-+
-+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
-+ // this accounts for change in emitted light that would cause an increase
-+ if (emittedLevel != 0) {
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+ }
-+ // this also accounts for a change in emitted light that would cause a decrease
-+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
-+ // as it checks all neighbours (even if current level is 0)
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ // always keep sided transparent false here, new block might be conditionally transparent which would
-+ // prevent us from decreasing sources in the directions where the new block is opaque
-+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
-+ // catch that and fix it.
-+ );
-+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ final BlockGetter world = lightAccess.getLevel();
-+ int level = (PlatformHooks.get().getLightEmission(centerState, world, this.recalcCenterPos)) & this.emittedLightMask;
-+
-+ if (level >= (15 - 1) || level > expect) {
-+ return level;
-+ }
-+
-+ final int opacity = Math.max(1, centerState.getLightBlock());
-+ if (opacity >= 15) {
-+ return level;
-+ }
-+ final BlockState conditionallyOpaqueState;
-+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ // passed transparency,
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
-+ final List<BlockPos> sources = new ArrayList<>();
-+
-+ final int offX = chunk.getPos().x << 4;
-+ final int offZ = chunk.getPos().z << 4;
-+
-+ final PlatformHooks platformHooks = PlatformHooks.get();
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
-+ final LevelChunkSection section = sections[sectionY - this.minSection];
-+ if (section.hasOnlyAir()) {
-+ // no sources in empty sections
-+ continue;
-+ }
-+ if (!section.maybeHas(platformHooks.maybeHasLightEmission())) {
-+ // no light sources in palette
-+ continue;
-+ }
-+ final PalettedContainer<BlockState> states = section.states;
-+ final int offY = sectionY << 4;
-+
-+ final BlockPos.MutableBlockPos mutablePos = this.lightEmissionPos;
-+ for (int index = 0; index < (16 * 16 * 16); ++index) {
-+ final BlockState state = states.get(index);
-+ mutablePos.set(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15));
-+
-+ if ((platformHooks.getLightEmission(state, world, mutablePos)) == 0) {
-+ continue;
-+ }
-+
-+ // index = x | (z << 4) | (y << 8)
-+ sources.add(mutablePos.immutable());
-+ }
-+ }
-+
-+ return sources;
-+ }
-+
-+ @Override
-+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ // setup sources
-+ final BlockGetter world = lightAccess.getLevel();
-+ final PlatformHooks platformHooks = PlatformHooks.get();
-+
-+ final int emittedMask = this.emittedLightMask;
-+ final List<BlockPos> positions = this.getSources(lightAccess, chunk);
-+ for (int i = 0, len = positions.size(); i < len; ++i) {
-+ final BlockPos pos = positions.get(i);
-+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
-+ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask;
-+
-+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
-+ // some other source is brighter
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLight & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+
-+
-+ // propagation wont set this for us
-+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
-+ }
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ // verify neighbour edges
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ } else {
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
-@@ -0,0 +1,440 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import net.minecraft.world.level.chunk.DataLayer;
-+import java.util.ArrayDeque;
-+import java.util.Arrays;
-+
-+// SWMR -> Single Writer Multi Reader Nibble Array
-+public final class SWMRNibbleArray {
-+
-+ /*
-+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
-+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
-+ * nibbles can be written to.
-+ *
-+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
-+ *
-+ * Initialised nibble - Has light data.
-+ */
-+
-+ protected static final int INIT_STATE_NULL = 0; // null
-+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
-+ protected static final int INIT_STATE_INIT = 2; // initialised
-+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
-+
-+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
-+ // this allows us to maintain only 1 byte array when we're not updating
-+ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
-+
-+ private static byte[] allocateBytes() {
-+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
-+ if (inPool != null) {
-+ return inPool;
-+ }
-+
-+ return new byte[ARRAY_SIZE];
-+ }
-+
-+ private static void freeBytes(final byte[] bytes) {
-+ WORKING_BYTES_POOL.get().addFirst(bytes);
-+ }
-+
-+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
-+ if (nibble == null) {
-+ return new SWMRNibbleArray(null, true);
-+ } else if (nibble.isEmpty()) {
-+ return new SWMRNibbleArray();
-+ } else {
-+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
-+ }
-+ }
-+
-+ protected int stateUpdating;
-+ protected volatile int stateVisible;
-+
-+ protected byte[] storageUpdating;
-+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
-+ protected volatile byte[] storageVisible;
-+
-+ public SWMRNibbleArray() {
-+ this(null, false); // lazy init
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes) {
-+ this(bytes, false);
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final int state) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
-+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
-+ }
-+ this.stateUpdating = this.stateVisible = state;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ StringBuilder stringBuilder = new StringBuilder();
-+ stringBuilder.append("State: ");
-+ switch (this.stateVisible) {
-+ case INIT_STATE_NULL:
-+ stringBuilder.append("null");
-+ break;
-+ case INIT_STATE_UNINIT:
-+ stringBuilder.append("uninitialised");
-+ break;
-+ case INIT_STATE_INIT:
-+ stringBuilder.append("initialised");
-+ break;
-+ case INIT_STATE_HIDDEN:
-+ stringBuilder.append("hidden");
-+ break;
-+ default:
-+ stringBuilder.append("unknown");
-+ break;
-+ }
-+ stringBuilder.append("\nData:\n");
-+
-+ final byte[] data = this.storageVisible;
-+ if (data != null) {
-+ for (int i = 0; i < 4096; ++i) {
-+ // Copied from NibbleArray#toString
-+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
-+
-+ stringBuilder.append(Integer.toHexString(level));
-+ if ((i & 15) == 15) {
-+ stringBuilder.append("\n");
-+ }
-+
-+ if ((i & 255) == 255) {
-+ stringBuilder.append("\n");
-+ }
-+ }
-+ } else {
-+ stringBuilder.append("null");
-+ }
-+
-+ return stringBuilder.toString();
-+ }
-+
-+ public SaveState getSaveState() {
-+ synchronized (this) {
-+ final int state = this.stateVisible;
-+ final byte[] data = this.storageVisible;
-+ if (state == INIT_STATE_NULL) {
-+ return null;
-+ }
-+ if (state == INIT_STATE_UNINIT) {
-+ return new SaveState(null, state);
-+ }
-+ final boolean zero = isAllZero(data);
-+ if (zero) {
-+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
-+ } else {
-+ return new SaveState(data.clone(), state);
-+ }
-+ }
-+ }
-+
-+ protected static boolean isAllZero(final byte[] data) {
-+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
-+ byte whole = data[i << 4];
-+
-+ for (int k = 1; k < (1 << 4); ++k) {
-+ whole |= data[(i << 4) | k];
-+ }
-+
-+ if (whole != 0) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: updating on src, updating on other
-+ public void extrudeLower(final SWMRNibbleArray other) {
-+ if (other.stateUpdating == INIT_STATE_NULL) {
-+ throw new IllegalArgumentException();
-+ }
-+
-+ if (other.storageUpdating == null) {
-+ this.setUninitialised();
-+ return;
-+ }
-+
-+ final byte[] src = other.storageUpdating;
-+ final byte[] into;
-+
-+ if (!this.updatingDirty) {
-+ if (this.storageUpdating != null) {
-+ into = this.storageUpdating = allocateBytes();
-+ } else {
-+ this.storageUpdating = into = allocateBytes();
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ } else {
-+ into = this.storageUpdating;
-+ }
-+
-+ final int start = 0;
-+ final int end = (15 | (15 << 4)) >>> 1;
-+
-+ /* x | (z << 4) | (y << 8) */
-+ for (int y = 0; y <= 15; ++y) {
-+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
-+ }
-+ }
-+
-+ // operation type: updating
-+ public void setFull() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setZero() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setNonNull() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_NULL) {
-+ return;
-+ }
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public void setNull() {
-+ this.stateUpdating = INIT_STATE_NULL;
-+ if (this.updatingDirty && this.storageUpdating != null) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setUninitialised() {
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ if (this.storageUpdating != null && this.updatingDirty) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setHidden() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_INIT) {
-+ this.setNull();
-+ } else {
-+ this.stateUpdating = INIT_STATE_HIDDEN;
-+ }
-+ }
-+
-+ // operation type: updating
-+ public boolean isDirty() {
-+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
-+ }
-+
-+ // operation type: updating
-+ public boolean isNullNibbleUpdating() {
-+ return this.stateUpdating == INIT_STATE_NULL;
-+ }
-+
-+ // operation type: visible
-+ public boolean isNullNibbleVisible() {
-+ return this.stateVisible == INIT_STATE_NULL;
-+ }
-+
-+ // opeartion type: updating
-+ public boolean isUninitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isUninitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isInitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isInitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenUpdating() {
-+ return this.stateUpdating == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenVisible() {
-+ return this.stateVisible == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ protected void swapUpdatingAndMarkDirty() {
-+ if (this.updatingDirty) {
-+ return;
-+ }
-+
-+ if (this.storageUpdating == null) {
-+ this.storageUpdating = allocateBytes();
-+ Arrays.fill(this.storageUpdating, (byte)0);
-+ } else {
-+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
-+ }
-+
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public boolean updateVisible() {
-+ if (!this.isDirty()) {
-+ return false;
-+ }
-+
-+ synchronized (this) {
-+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
-+ this.storageVisible = null;
-+ } else {
-+ if (this.storageVisible == null) {
-+ this.storageVisible = this.storageUpdating.clone();
-+ } else {
-+ if (this.storageUpdating != this.storageVisible) {
-+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
-+ }
-+ }
-+
-+ if (this.storageUpdating != this.storageVisible) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = this.storageVisible;
-+ }
-+ this.updatingDirty = false;
-+ this.stateVisible = this.stateUpdating;
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: visible
-+ public DataLayer toVanillaNibble() {
-+ synchronized (this) {
-+ switch (this.stateVisible) {
-+ case INIT_STATE_HIDDEN:
-+ case INIT_STATE_NULL:
-+ return null;
-+ case INIT_STATE_UNINIT:
-+ return new DataLayer();
-+ case INIT_STATE_INIT:
-+ return new DataLayer(this.storageVisible.clone());
-+ default:
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ /* x | (z << 4) | (y << 8) */
-+
-+ // operation type: updating
-+ public int getUpdating(final int x, final int y, final int z) {
-+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: updating
-+ public int getUpdating(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] bytes = this.storageUpdating;
-+ if (bytes == null) {
-+ return 0;
-+ }
-+ final byte value = bytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int x, final int y, final int z) {
-+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] visibleBytes = this.storageVisible;
-+ if (visibleBytes == null) {
-+ return 0;
-+ }
-+ final byte value = visibleBytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int x, final int y, final int z, final int value) {
-+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int index, final int value) {
-+ if (!this.updatingDirty) {
-+ this.swapUpdatingAndMarkDirty();
-+ }
-+ final int shift = (index & 1) << 2;
-+ final int i = index >>> 1;
-+
-+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
-+ }
-+
-+ public static final class SaveState {
-+
-+ public final byte[] data;
-+ public final int state;
-+
-+ public SaveState(final byte[] data, final int state) {
-+ this.data = data;
-+ this.state = state;
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-@@ -0,0 +1,681 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
-+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.Set;
-+
-+public final class SkyStarLightEngine extends StarLightEngine {
-+
-+ /*
-+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
-+
-+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
-+
-+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
-+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
-+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
-+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
-+ How do we solve this?
-+
-+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
-+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
-+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
-+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
-+ to see if any of its nibbles need to be de-initialised.
-+
-+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
-+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
-+
-+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
-+ around those.
-+ */
-+
-+ protected final int[] heightMapBlockChange = new int[16 * 16];
-+ {
-+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
-+ }
-+
-+ protected final boolean[] nullPropagationCheckCache;
-+
-+ public SkyStarLightEngine(final Level world) {
-+ super(true, world);
-+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
-+ }
-+ }
-+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ nibble.setNull();
-+ }
-+ }
-+
-+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
-+ if (!currNibble.isNullNibbleUpdating()) {
-+ // already initialised
-+ return;
-+ }
-+
-+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = this.minLightSection - 1;
-+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
-+ if (emptinessMap == null) {
-+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
-+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
-+ if (current == null || current.hasOnlyAir()) {
-+ continue;
-+ }
-+ } else {
-+ if (emptinessMap[currY - this.minSection]) {
-+ continue;
-+ }
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (chunkY > lowestY) {
-+ // we need to set this one to full
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ nibble.setNonNull();
-+ nibble.setFull();
-+ return;
-+ }
-+
-+ if (extrude) {
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ currNibble.setNonNull();
-+ currNibble.extrudeLower(nibble);
-+ break;
-+ }
-+ }
-+ } else {
-+ currNibble.setNonNull();
-+ }
-+ }
-+
-+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (nibble != null && nibble.isNullNibbleUpdating()) {
-+ // stop propagation in these areas
-+ this.nibbleCache[index] = null;
-+ nibble.updateVisible();
-+ }
-+ }
-+ }
-+
-+ // rets whether neighbours were init'd
-+
-+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final boolean extrudeInitialised) {
-+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
-+ // non-null. Propagation to these neighbours is necessary.
-+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
-+ // this nibble would be initialised). So, we don't have to initialise
-+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
-+ // to the neighbours on this horizontal plane are blocked.
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
-+ return false;
-+ }
-+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
-+
-+ // check horizontal neighbours
-+ boolean needInitNeighbours = false;
-+ neighbour_search:
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ needInitNeighbours = true;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+
-+ if (needInitNeighbours) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
-+ }
-+ }
-+ }
-+
-+ return needInitNeighbours;
-+ }
-+
-+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
-+ final int chunkX = worldX >> 4;
-+ int chunkY = worldY >> 4;
-+ final int chunkZ = worldZ >> 4;
-+
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, worldY, worldZ);
-+ }
-+
-+ for (;;) {
-+ if (++chunkY > this.maxLightSection) {
-+ return 15;
-+ }
-+
-+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, 0, worldZ);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return ((StarlightChunk)chunk).starlight$getSkyNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ ((StarlightChunk)chunk).starlight$setSkyNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ // can only use chunks for sky stuff if their sections have been init'd
-+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
-+ final int toSection) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (int y = toSection; y >= fromSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ final int y = (int)iterator.nextShort();
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, sections);
-+ }
-+
-+ @Override
-+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change direction of propagation
-+
-+ // same logic applies from BlockStarLightEngine#checkBlock
-+
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+
-+ if (currentLevel == 15) {
-+ // must re-propagate clobbered source
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
-+ );
-+ } else {
-+ this.setLightLevel(worldX, worldY, worldZ, 0);
-+ }
-+
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ );
-+ }
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ if (expect == 15) {
-+ return expect;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+
-+ final BlockState conditionallyOpaqueState;
-+ final int opacity = Math.max(1, centerState.getLightBlock());
-+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+
-+ int level = 0;
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+
-+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ this.rewriteNibbleCacheForSkylight(atChunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final int chunkX = atChunk.getPos().x;
-+ final int chunkZ = atChunk.getPos().z;
-+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
-+
-+ // setup heightmap for changes
-+ for (final BlockPos pos : positions) {
-+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
-+ final int curr = this.heightMapBlockChange[index];
-+ if (pos.getY() > curr) {
-+ this.heightMapBlockChange[index] = pos.getY();
-+ }
-+ }
-+
-+ // note: light sets are delayed while processing skylight source changes due to how
-+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
-+ // below nibbles are initialised they aren't reading from partially modified nibbles
-+
-+ // now we can recalculate the sources for the changed columns
-+ for (int index = 0; index < (16 * 16); ++index) {
-+ final int maxY = this.heightMapBlockChange[index];
-+ if (maxY == Integer.MIN_VALUE) {
-+ // not changed
-+ continue;
-+ }
-+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
-+
-+ final int columnX = (index & 15) | (chunkX << 4);
-+ final int columnZ = (index >>> 4) | (chunkZ << 4);
-+
-+ // try and propagate from the above y
-+ // delay light set until after processing all sources to setup
-+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
-+
-+ // maxPropagationY is now the highest block that could not be propagated to
-+
-+ // remove all sources below that are 15
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
-+
-+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
-+ if ((currY & 15) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
-+ }
-+
-+ // ensure section below is always checked
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
-+ if (nibble == null) {
-+ // advance currY to the the top of the section below
-+ currY = (currY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+ continue;
-+ }
-+
-+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
-+ break;
-+ }
-+
-+ // delay light set until after processing all sources to setup
-+ this.appendToDecreaseQueue(
-+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // do not set transparent blocks for the same reason we don't in the checkBlock method
-+ );
-+ }
-+ }
-+ }
-+
-+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
-+ // immediate light value
-+ this.processDelayedIncreases();
-+ this.processDelayedDecreases();
-+
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected final int[] heightMapGen = new int[32 * 32];
-+
-+ @Override
-+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ int highestNonEmptySection = this.maxSection;
-+ while (highestNonEmptySection == (this.minSection - 1) ||
-+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
-+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
-+ // try propagate FULL to neighbours
-+
-+ // check neighbours to see if we need to propagate into them
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourX = chunkX + direction.x;
-+ final int neighbourZ = chunkZ + direction.z;
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
-+ if (neighbourNibble == null) {
-+ // unloaded neighbour
-+ // most of the time we fall here
-+ continue;
-+ }
-+
-+ // it looks like we need to propagate into the neighbour
-+
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (direction.x != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (direction.z < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
-+
-+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
-+ );
-+ }
-+ }
-+ }
-+
-+ if (highestNonEmptySection-- == (this.minSection - 1)) {
-+ break;
-+ }
-+ }
-+
-+ if (highestNonEmptySection >= this.minSection) {
-+ // fill out our other sources
-+ final int minX = chunkPos.x << 4;
-+ final int maxX = chunkPos.x << 4 | 15;
-+ final int minZ = chunkPos.z << 4;
-+ final int maxZ = chunkPos.z << 4 | 15;
-+ final int startY = highestNonEmptySection << 4 | 15;
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
-+ }
-+ }
-+ } // else: apparently the chunk is empty
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ // no need to rewrite the nibble cache again
-+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+ } else {
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+
-+ protected final void processDelayedIncreases() {
-+ // copied from performLightIncrease
-+ final long[] queue = this.increaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+ }
-+
-+ protected final void processDelayedDecreases() {
-+ // copied from performLightDecrease
-+ final long[] queue = this.decreaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+
-+ this.setLightLevel(posX, posY, posZ, 0);
-+ }
-+ }
-+
-+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
-+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
-+ // clobbering the light values will result in broken propagation)
-+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
-+ final boolean extrudeInitialised, final boolean delayLightSet) {
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
-+
-+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
-+ return startY;
-+ }
-+
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+
-+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
-+
-+ for (;startY >= (this.minLightSection << 4); --startY) {
-+ if ((startY & 15) == 15) {
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+ }
-+ final BlockState current = this.getBlockState(worldX, startY, worldZ);
-+
-+ final VoxelShape fromShape;
-+ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) {
-+ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms);
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ // above wont let us propagate
-+ break;
-+ }
-+ } else {
-+ fromShape = Shapes.empty();
-+ }
-+
-+ // does light propagate from the top down?
-+ long flags = 0L;
-+ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = current.getFaceOcclusionShape(AxisDirection.POSITIVE_Y.nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ // can't propagate here, we're done on this column.
-+ break;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = current.getLightBlock();
-+ if (opacity > 0) {
-+ // let the queued value (if any) handle it from here.
-+ break;
-+ }
-+
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | flags
-+ );
-+
-+ above = current;
-+
-+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
-+ // we skip empty sections here, as this is just an easy way of making sure the above block
-+ // can propagate through air.
-+
-+ // nothing can propagate in null sections, remove the queue entry for it
-+ --this.increaseQueueInitialLength;
-+
-+ // advance currY to the the top of the section below
-+ startY = (startY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+
-+ // make sure this is marked as AIR
-+ above = AIR_BLOCK_STATE;
-+ } else if (!delayLightSet) {
-+ this.setLightLevel(worldX, startY, worldZ, 15);
-+ }
-+ }
-+
-+ return startY;
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-@@ -0,0 +1,1438 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.LevelHeightAccessor;
-+import net.minecraft.world.level.LightLayer;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public abstract class StarLightEngine {
-+
-+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
-+
-+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
-+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
-+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
-+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
-+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
-+ };
-+
-+ protected static enum AxisDirection {
-+
-+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
-+ POSITIVE_X(1, 0, 0, Direction.EAST) , NEGATIVE_X(-1, 0, 0, Direction.WEST),
-+ POSITIVE_Z(0, 0, 1, Direction.SOUTH), NEGATIVE_Z(0, 0, -1, Direction.NORTH),
-+ POSITIVE_Y(0, 1, 0, Direction.UP) , NEGATIVE_Y(0, -1, 0, Direction.DOWN);
-+
-+ static {
-+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
-+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
-+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
-+ }
-+
-+ protected AxisDirection opposite;
-+
-+ public final int x;
-+ public final int y;
-+ public final int z;
-+ public final Direction nms;
-+ public final long everythingButThisDirection;
-+ public final long everythingButTheOppositeDirection;
-+
-+ AxisDirection(final int x, final int y, final int z, final Direction nms) {
-+ this.x = x;
-+ this.y = y;
-+ this.z = z;
-+ this.nms = nms;
-+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
-+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
-+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
-+ }
-+
-+ public AxisDirection getOpposite() {
-+ return this.opposite;
-+ }
-+ }
-+
-+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
-+ // for explaining how light propagates via breadth-first search
-+
-+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
-+ // exactly how the vanilla light engine should behave for minecraft.
-+
-+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
-+ protected final LevelChunkSection[] sectionCache;
-+
-+ // the exact same as above, except for storing fast access to SWMRNibbleArray
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final SWMRNibbleArray[] nibbleCache;
-+
-+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final boolean[] notifyUpdateCache;
-+
-+ // always initialsed during start of lighting.
-+ // index = x + (z * 5)
-+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
-+
-+ // index = x + (z * 5)
-+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
-+
-+ protected final BlockPos.MutableBlockPos lightEmissionPos = new BlockPos.MutableBlockPos();
-+
-+ protected int encodeOffsetX;
-+ protected int encodeOffsetY;
-+ protected int encodeOffsetZ;
-+
-+ protected int coordinateOffset;
-+
-+ protected int chunkOffsetX;
-+ protected int chunkOffsetY;
-+ protected int chunkOffsetZ;
-+
-+ protected int chunkIndexOffset;
-+ protected int chunkSectionIndexOffset;
-+
-+ protected final boolean skylightPropagator;
-+ protected final int emittedLightMask;
-+ protected final boolean isClientSide;
-+
-+ protected final Level world;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+ protected final int minSection;
-+ protected final int maxSection;
-+
-+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
-+ this.skylightPropagator = skylightPropagator;
-+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
-+ this.isClientSide = world.isClientSide;
-+ this.world = world;
-+ this.minLightSection = WorldUtil.getMinLightSection(world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+
-+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ }
-+
-+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
-+ // 31 = center + encodeOffset
-+ this.encodeOffsetX = 31 - centerX;
-+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
-+ this.encodeOffsetZ = 31 - centerZ;
-+
-+ // coordinateIndex = x | (z << 6) | (y << 12)
-+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
-+
-+ // 2 = (centerX >> 4) + chunkOffset
-+ this.chunkOffsetX = 2 - (centerX >> 4);
-+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
-+ this.chunkOffsetZ = 2 - (centerZ >> 4);
-+
-+ // chunk index = x + (5 * z)
-+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
-+
-+ // chunk section index = x + (5 * z) + ((5*5) * y)
-+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
-+ }
-+
-+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
-+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
-+ final int centerChunkX = centerX >> 4;
-+ final int centerChunkY = centerY >> 4;
-+ final int centerChunkZ = centerZ >> 4;
-+
-+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
-+
-+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
-+
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ final int cx = centerChunkX + dx;
-+ final int cz = centerChunkZ + dz;
-+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
-+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
-+
-+ if (chunk == null) {
-+ if (relaxed | isTwoRadius) {
-+ continue;
-+ }
-+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
-+ }
-+
-+ if (!this.canUseChunk(chunk)) {
-+ continue;
-+ }
-+
-+ this.setChunkInCache(cx, cz, chunk);
-+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
-+ if (!isTwoRadius) {
-+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
-+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
-+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
-+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
-+ }
-+
-+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
-+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
-+ }
-+
-+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
-+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
-+ }
-+ }
-+
-+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
-+
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
-+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
-+ }
-+
-+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
-+ }
-+ }
-+
-+ protected final void updateVisible(final LightChunkGetter lightAccess) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
-+ continue;
-+ }
-+
-+ final int chunkX = (index % 5) - this.chunkOffsetX;
-+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
-+ final int ySections = this.maxSection - this.minSection + 1;
-+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
-+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
-+ }
-+ }
-+ }
-+
-+ protected final void destroyCaches() {
-+ Arrays.fill(this.sectionCache, null);
-+ Arrays.fill(this.nibbleCache, null);
-+ Arrays.fill(this.chunkCache, null);
-+ Arrays.fill(this.emptinessMapCache, null);
-+ if (this.isClientSide) {
-+ Arrays.fill(this.notifyUpdateCache, false);
-+ }
-+ }
-+
-+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
-+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
-+ final LevelChunkSection section = this.sectionCache[sectionIndex];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
-+ }
-+
-+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
-+ }
-+
-+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
-+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set(localIndex, level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
-+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
-+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
-+ }
-+
-+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
-+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
-+ }
-+
-+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
-+
-+ for (int i = 0, len = ret.length; i < len; ++i) {
-+ ret[i] = new SWMRNibbleArray(null, true);
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
-+
-+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
-+
-+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
-+
-+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
-+
-+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
-+
-+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Set<BlockPos> positions, final Boolean[] changedSections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ if (changedSections != null) {
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ }
-+ if (!positions.isEmpty()) {
-+ this.propagateBlockChanges(lightAccess, chunk, positions);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
-+
-+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
-+
-+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
-+ // if ret == expect, then expect is the correct light value for pos
-+ // if ret < expect, then ret is the real light value
-+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect);
-+
-+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
-+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
-+
-+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (currNibble == null) {
-+ return;
-+ }
-+
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ chunkY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null) {
-+ continue;
-+ }
-+
-+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
-+ // both are zero, nothing to check.
-+ continue;
-+ }
-+
-+ // this chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ int centerDelayedChecks = 0;
-+ int neighbourDelayedChecks = 0;
-+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int neighbourX = currX + neighbourOffX;
-+ final int neighbourZ = currZ + neighbourOffZ;
-+
-+ final int currentIndex = (currX & 15) |
-+ ((currZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int currentLevel = currNibble.getUpdating(currentIndex);
-+
-+ final int neighbourIndex =
-+ (neighbourX & 15) |
-+ ((neighbourZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
-+
-+ // the checks are delayed because the checkBlock method clobbers light values - which then
-+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
-+ // way, they do have a negative performance impact due to simply queueing more values
-+
-+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
-+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
-+ }
-+
-+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
-+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
-+ }
-+ }
-+ }
-+
-+ final int currentChunkOffX = chunkX << 4;
-+ final int currentChunkOffZ = chunkZ << 4;
-+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
-+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
-+ final int chunkOffY = chunkY << 4;
-+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
-+ // try to queue neighbouring data together
-+ // index = x | (z << 4) | (y << 8)
-+ if (i < centerDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
-+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ currentChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ if (i < neighbourDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
-+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
-+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
-+ // This does not resolve skylight source problems.
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
-+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
-+ if (currNibble == null) {
-+ continue;
-+ }
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ currSectionY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
-+ // can't pull from 0
-+ continue;
-+ }
-+
-+ // neighbour chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = (chunkX << 4) - 1;
-+ } else {
-+ startX = (chunkX << 4) + 16;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = (chunkZ << 4) - 1;
-+ } else {
-+ startZ = (chunkZ << 4) + 16;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int level = neighbourNibble.getUpdating(
-+ (currX & 15)
-+ | ((currZ & 15) << 4)
-+ | ((currY & 15) << 8)
-+ );
-+
-+ if (level <= 1) {
-+ // nothing to propagate
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((level & 0xFL) << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
-+ );
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ final Boolean[] ret = new Boolean[sections.length];
-+
-+ for (int i = 0; i < sections.length; ++i) {
-+ if (sections[i] == null || sections[i].hasOnlyAir()) {
-+ ret[i] = Boolean.TRUE;
-+ } else {
-+ ret[i] = Boolean.FALSE;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Boolean[] emptinessChanges) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
-+
-+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // subclasses are guaranteed that this is always called before a changed block set
-+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
-+ // rets non-null when the emptiness map changed and needs to be updated
-+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final Boolean[] emptinessChanges, final boolean unlit) {
-+ final Level world = (Level)lightAccess.getLevel();
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+
-+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+ boolean[] ret = null;
-+ final boolean needsInit = unlit || chunkEmptinessMap == null;
-+ if (needsInit) {
-+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
-+ }
-+
-+ // update emptiness map
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ if (valueBoxed == null) {
-+ if (!needsInit) {
-+ continue;
-+ }
-+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
-+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
-+ }
-+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
-+ }
-+
-+ // now init neighbour nibbles
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ final int sectionY = sectionIndex + this.minSection;
-+ if (valueBoxed == null) {
-+ continue;
-+ }
-+
-+ final boolean empty = valueBoxed.booleanValue();
-+
-+ if (empty) {
-+ continue;
-+ }
-+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // if we're not empty, we also need to initialise nibbles
-+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ for (int dy = 1; dy >= -1; --dy) {
-+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ // check for de-init and lazy-init
-+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
-+ // init checks.
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // does this neighbour have 1 radius loaded?
-+ boolean neighboursLoaded = true;
-+ neighbour_loaded_search:
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
-+ neighboursLoaded = false;
-+ break neighbour_loaded_search;
-+ }
-+ }
-+ }
-+
-+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
-+ // check neighbours to see if we need to de-init this one
-+ boolean allEmpty = true;
-+ neighbour_search:
-+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int y = sectionY + dy2;
-+ if (y < this.minSection || y > this.maxSection) {
-+ // empty
-+ continue;
-+ }
-+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
-+ if (emptinessMap != null) {
-+ if (!emptinessMap[y - this.minSection]) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ } else {
-+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
-+ if (section != null && !section.hasOnlyAir()) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ if (allEmpty & neighboursLoaded) {
-+ // can only de-init when neighbours are loaded
-+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
-+ // to be correct
-+
-+ // all were empty, so de-init
-+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
-+ } else if (!allEmpty) {
-+ // must init
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, sections);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
-+ // chunks light values with respect to neighbours
-+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
-+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
-+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
-+
-+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+
-+ try {
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.lightChunk(lightAccess, chunk, true);
-+ this.setNibbles(chunk, nibbles);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
-+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
-+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
-+ // the region of chunks to relight
-+ // it's required that tickets are added for each chunk to keep them loaded
-+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
-+
-+ final int[] neighbourLightOrder = new int[] {
-+ // d = 0
-+ 0, 0,
-+ // d = 1
-+ -1, 0,
-+ 0, -1,
-+ 1, 0,
-+ 0, 1,
-+ // d = 2
-+ -1, 1,
-+ 1, 1,
-+ -1, -1,
-+ 1, -1,
-+ };
-+
-+ int lightCalls = 0;
-+
-+ for (final ChunkPos chunkPos : chunks) {
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
-+ if (chunk == null || !this.canUseChunk(chunk)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
-+ final int dx = neighbourLightOrder[i];
-+ final int dz = neighbourLightOrder[i + 1];
-+ final int neighbourX = dx + chunkX;
-+ final int neighbourZ = dz + chunkZ;
-+
-+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
-+ if (neighbour == null || !this.canUseChunk(neighbour)) {
-+ continue;
-+ }
-+
-+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
-+ // lit already called for neighbour, no need to light it now
-+ continue;
-+ }
-+
-+ // light neighbour chunk
-+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
-+ try {
-+ // insert all neighbouring chunks for this neighbour that we have data for
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int neighbourX2 = neighbourX + dx2;
-+ final int neighbourZ2 = neighbourZ + dz2;
-+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
-+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
-+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
-+ if (nibbles == null) {
-+ // we haven't lit this chunk
-+ continue;
-+ }
-+
-+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
-+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
-+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
-+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
-+ }
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ // now insert the neighbour chunk and light it
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
-+ nibblesByChunk.put(key, nibbles);
-+
-+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
-+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
-+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
-+
-+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
-+ emptinessMapByChunk.put(key, neighbourEmptiness);
-+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
-+ this.setEmptinessMap(neighbour, neighbourEmptiness);
-+ }
-+
-+ this.lightChunk(lightAccess, neighbour, false);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // done lighting all neighbours, so the chunk is now fully lit
-+
-+ // make sure nibbles are fully updated before calling back
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ for (final SWMRNibbleArray nibble : nibbles) {
-+ nibble.updateVisible();
-+ }
-+
-+ this.setNibbles(chunk, nibbles);
-+
-+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ));
-+ }
-+
-+ // now do callback
-+ if (chunkLightCallback != null) {
-+ chunkLightCallback.accept(chunkPos);
-+ }
-+ ++lightCalls;
-+ }
-+
-+ if (onComplete != null) {
-+ onComplete.accept(lightCalls);
-+ }
-+ }
-+
-+ // contains:
-+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
-+ // next 4 bits: propagated light level (0, 15]
-+ // next 6 bits: propagation direction bitset
-+ // next 24 bits: unused
-+ // last 3 bits: state flags
-+ // state flags:
-+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
-+ // updates for block sources
-+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
-+ // whether the propagation needs to check if its current level is equal to the expected level
-+ // used only in increase propagation
-+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
-+ // whether the propagation needs to consider if its block is conditionally transparent
-+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
-+
-+ protected long[] increaseQueue = new long[16 * 16 * 16];
-+ protected int increaseQueueInitialLength;
-+ protected long[] decreaseQueue = new long[16 * 16 * 16];
-+ protected int decreaseQueueInitialLength;
-+
-+ protected final long[] resizeIncreaseQueue() {
-+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
-+ }
-+
-+ protected final long[] resizeDecreaseQueue() {
-+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
-+ }
-+
-+ protected final void appendToIncreaseQueue(final long value) {
-+ final int idx = this.increaseQueueInitialLength++;
-+ long[] queue = this.increaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected final void appendToDecreaseQueue(final long value) {
-+ final int idx = this.decreaseQueueInitialLength++;
-+ long[] queue = this.decreaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
-+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
-+ static {
-+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
-+ final List<AxisDirection> directions = new ArrayList<>();
-+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
-+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
-+ }
-+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
-+ }
-+ }
-+
-+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.increaseQueueInitialLength;
-+ this.increaseQueueInitialLength = 0;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
-+
-+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
-+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
-+ // not at the level we expect, so something changed.
-+ continue;
-+ }
-+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
-+ // these are used to restore block sources after a propagation decrease
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want or unloaded
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.decreaseQueue;
-+ long[] increaseQueue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.decreaseQueueInitialLength;
-+ this.decreaseQueueInitialLength = 0;
-+ int increaseQueueLength = this.increaseQueueInitialLength;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ final PlatformHooks platformHooks = PlatformHooks.get();
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ this.lightEmissionPos.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ this.lightEmissionPos.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = (platformHooks.getLightEmission(blockState, world, this.lightEmissionPos)) & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+
-+ // propagate sources we clobbered
-+ this.increaseQueueInitialLength = increaseQueueLength;
-+ this.performLightIncrease(lightAccess);
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-@@ -0,0 +1,931 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
-+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.server.level.ChunkLevel;
-+import net.minecraft.server.level.FullChunkStatus;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.DataLayer;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import net.minecraft.world.level.lighting.LevelLightEngine;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.HashSet;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.function.BooleanSupplier;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public final class StarLightInterface {
-+
-+ public static final TicketType<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo);
-+ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT);
-+ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input
-+ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL;
-+
-+ /**
-+ * Can be {@code null}, indicating the light is all empty.
-+ */
-+ public final Level world;
-+ public final LightChunkGetter lightAccess;
-+
-+ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
-+ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
-+
-+ private final LightQueue lightQueue;
-+
-+ private final LayerLightEventListener skyReader;
-+ private final LayerLightEventListener blockReader;
-+ private final boolean isClientSide;
-+
-+ public final int minSection;
-+ public final int maxSection;
-+ public final int minLightSection;
-+ public final int maxLightSection;
-+
-+ public final LevelLightEngine lightEngine;
-+
-+ private final boolean hasBlockLight;
-+ private final boolean hasSkyLight;
-+
-+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
-+ this.lightAccess = lightAccess;
-+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
-+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.isClientSide = !(this.world instanceof ServerLevel);
-+ if (this.world == null) {
-+ this.minSection = -4;
-+ this.maxSection = 19;
-+ this.minLightSection = -5;
-+ this.maxLightSection = 20;
-+ } else {
-+ this.minSection = WorldUtil.getMinSection(this.world);
-+ this.maxSection = WorldUtil.getMaxSection(this.world);
-+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
-+ }
-+
-+ if (this.world instanceof ServerLevel) {
-+ this.lightQueue = new ServerLightQueue(this);
-+ } else {
-+ this.lightQueue = new ClientLightQueue(this);
-+ }
-+
-+ this.lightEngine = lightEngine;
-+ this.hasBlockLight = hasBlockLight;
-+ this.hasSkyLight = hasSkyLight;
-+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return null;
-+ }
-+
-+ final int sectionY = pos.getY();
-+
-+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
-+ return null;
-+ }
-+
-+ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) {
-+ return null;
-+ }
-+
-+ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+
-+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
-+ return null;
-+ }
-+
-+ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ }
-+
-+ public ClientLightQueue getClientLightQueue() {
-+ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) {
-+ return clientLightQueue;
-+ }
-+ return null;
-+ }
-+
-+ public ServerLightQueue getServerLightQueue() {
-+ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) {
-+ return serverLightQueue;
-+ }
-+ return null;
-+ }
-+
-+ public boolean hasSkyLight() {
-+ return this.hasSkyLight;
-+ }
-+
-+ public boolean hasBlockLight() {
-+ return this.hasBlockLight;
-+ }
-+
-+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasSkyLight) {
-+ return 0;
-+ }
-+ final int x = blockPos.getX();
-+ int y = blockPos.getY();
-+ final int z = blockPos.getZ();
-+
-+ final int minSection = this.minSection;
-+ final int maxSection = this.maxSection;
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return 15;
-+ }
-+
-+ int sectionY = y >> 4;
-+
-+ if (sectionY > maxLightSection) {
-+ return 15;
-+ }
-+
-+ if (sectionY < minLightSection) {
-+ sectionY = minLightSection;
-+ y = sectionY << 4;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
-+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
-+
-+ if (!immediate.isNullNibbleVisible()) {
-+ return immediate.getVisible(x, y, z);
-+ }
-+
-+ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
-+
-+ if (emptinessMap == null) {
-+ return 15;
-+ }
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = minLightSection - 1;
-+ for (int currY = maxSection; currY >= minSection; --currY) {
-+ if (emptinessMap[currY - minSection]) {
-+ continue;
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (sectionY > lowestY) {
-+ return 15;
-+ }
-+
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
-+ if (!nibble.isNullNibbleVisible()) {
-+ return nibble.getVisible(x, 0, z);
-+ }
-+ }
-+
-+ // should never reach here
-+ return 15;
-+ }
-+
-+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasBlockLight) {
-+ return 0;
-+ }
-+ final int y = blockPos.getY();
-+ final int cy = y >> 4;
-+
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (cy < minLightSection || cy > maxLightSection) {
-+ return 0;
-+ }
-+
-+ if (chunk == null) {
-+ return 0;
-+ }
-+
-+ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection];
-+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
-+ }
-+
-+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
-+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
-+
-+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
-+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
-+ if (sky == 15) {
-+ return 15;
-+ }
-+ final int block = this.getBlockLightValue(pos, chunk);
-+ return Math.max(sky, block);
-+ }
-+
-+ public LayerLightEventListener getSkyReader() {
-+ return this.skyReader;
-+ }
-+
-+ public LayerLightEventListener getBlockReader() {
-+ return this.blockReader;
-+ }
-+
-+ public boolean isClientSide() {
-+ return this.isClientSide;
-+ }
-+
-+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
-+ if (this.world == null) {
-+ // empty world
-+ return null;
-+ }
-+ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
-+ }
-+
-+ public boolean hasUpdates() {
-+ return !this.lightQueue.isEmpty();
-+ }
-+
-+ public Level getWorld() {
-+ return this.world;
-+ }
-+
-+ public LightChunkGetter getLightAccess() {
-+ return this.lightAccess;
-+ }
-+
-+ public SkyStarLightEngine getSkyLightEngine() {
-+ if (this.cachedSkyPropagators == null) {
-+ return null;
-+ }
-+ final SkyStarLightEngine ret;
-+ synchronized (this.cachedSkyPropagators) {
-+ ret = this.cachedSkyPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new SkyStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ public void releaseSkyLightEngine(final SkyStarLightEngine engine) {
-+ if (this.cachedSkyPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedSkyPropagators) {
-+ this.cachedSkyPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ public BlockStarLightEngine getBlockLightEngine() {
-+ if (this.cachedBlockPropagators == null) {
-+ return null;
-+ }
-+ final BlockStarLightEngine ret;
-+ synchronized (this.cachedBlockPropagators) {
-+ ret = this.cachedBlockPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new BlockStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ public void releaseBlockLightEngine(final BlockStarLightEngine engine) {
-+ if (this.cachedBlockPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedBlockPropagators) {
-+ this.cachedBlockPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
-+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueBlockChange(pos);
-+ }
-+
-+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ if (this.world == null) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
-+ }
-+
-+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
-+ final IntConsumer onComplete) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
-+ blockEngine == null ? onComplete : null);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
-+ this.checkSkyEdges(chunkX, chunkZ);
-+ this.checkBlockEdges(chunkX, chunkZ);
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void propagateChanges() {
-+ final LightQueue lightQueue = this.lightQueue;
-+ if (lightQueue instanceof ClientLightQueue clientLightQueue) {
-+ clientLightQueue.drainTasks();
-+ } // else: invalid usage, although we won't throw because mods...
-+ }
-+
-+ public static abstract class LightQueue {
-+
-+ protected final StarLightInterface lightInterface;
-+
-+ public LightQueue(final StarLightInterface lightInterface) {
-+ this.lightInterface = lightInterface;
-+ }
-+
-+ public abstract boolean isEmpty();
-+
-+ public abstract ChunkTasks queueBlockChange(final BlockPos pos);
-+
-+ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue);
-+
-+ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections);
-+
-+ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections);
-+
-+ public static abstract class ChunkTasks implements Runnable {
-+
-+ public final long chunkCoordinate;
-+
-+ protected final StarLightInterface lightEngine;
-+ protected final LightQueue queue;
-+ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>();
-+ protected final Set<BlockPos> changedPositions = new HashSet<>();
-+ protected Boolean[] changedSectionSet;
-+ protected ShortOpenHashSet queuedEdgeChecksSky;
-+ protected ShortOpenHashSet queuedEdgeChecksBlock;
-+ protected List<BooleanSupplier> lightTasks;
-+
-+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) {
-+ this.chunkCoordinate = chunkCoordinate;
-+ this.lightEngine = lightEngine;
-+ this.queue = queue;
-+ }
-+
-+ @Override
-+ public abstract void run();
-+
-+ public void queueOrRunTask(final Runnable run) {
-+ if (!this.onComplete.add(run)) {
-+ run.run();
-+ }
-+ }
-+
-+ protected void addChangedPosition(final BlockPos pos) {
-+ this.changedPositions.add(pos.immutable());
-+ }
-+
-+ protected void setChangedSection(final int y, final Boolean newEmptyValue) {
-+ if (this.changedSectionSet == null) {
-+ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1];
-+ }
-+ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue;
-+ }
-+
-+ protected void addLightTask(final BooleanSupplier lightTask) {
-+ if (this.lightTasks == null) {
-+ this.lightTasks = new ArrayList<>();
-+ }
-+ this.lightTasks.add(lightTask);
-+ }
-+
-+ protected void addEdgeChecksSky(final ShortCollection values) {
-+ if (this.queuedEdgeChecksSky == null) {
-+ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size()));
-+ }
-+ this.queuedEdgeChecksSky.addAll(values);
-+ }
-+
-+ protected void addEdgeChecksBlock(final ShortCollection values) {
-+ if (this.queuedEdgeChecksBlock == null) {
-+ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size()));
-+ }
-+ this.queuedEdgeChecksBlock.addAll(values);
-+ }
-+
-+ protected final void runTasks() {
-+ boolean litChunk = false;
-+ if (this.lightTasks != null) {
-+ for (final BooleanSupplier run : this.lightTasks) {
-+ if (run.getAsBoolean()) {
-+ litChunk = true;
-+ break;
-+ }
-+ }
-+ }
-+
-+ if (!litChunk) {
-+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine();
-+ try {
-+ final long coordinate = this.chunkCoordinate;
-+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
-+
-+ final Set<BlockPos> positions = this.changedPositions;
-+ final Boolean[] sectionChanges = this.changedSectionSet;
-+
-+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
-+ }
-+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
-+ }
-+
-+ if (skyEngine != null && this.queuedEdgeChecksSky != null) {
-+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky);
-+ }
-+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) {
-+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock);
-+ }
-+ } finally {
-+ this.lightEngine.releaseSkyLightEngine(skyEngine);
-+ this.lightEngine.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ Runnable run;
-+ while ((run = this.onComplete.pollOrBlockAdds()) != null) {
-+ run.run();
-+ }
-+ }
-+ }
-+ }
-+
-+ public static final class ClientLightQueue extends LightQueue {
-+
-+ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
-+
-+ public ClientLightQueue(final StarLightInterface lightInterface) {
-+ super(lightInterface);
-+ }
-+
-+ @Override
-+ public synchronized boolean isEmpty() {
-+ return this.chunkTasks.isEmpty();
-+ }
-+
-+ // must hold synchronized lock on this object
-+ private ClientChunkTasks getOrCreate(final long key) {
-+ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> {
-+ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this);
-+ });
-+ }
-+
-+ @Override
-+ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) {
-+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
-+ tasks.addChangedPosition(pos);
-+ return tasks;
-+ }
-+
-+ @Override
-+ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
-+
-+ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
-+
-+ return tasks;
-+ }
-+
-+ @Override
-+ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
-+
-+ tasks.addEdgeChecksSky(sections);
-+
-+ return tasks;
-+ }
-+
-+ @Override
-+ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
-+
-+ tasks.addEdgeChecksBlock(sections);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized ClientChunkTasks removeFirstTask() {
-+ if (this.chunkTasks.isEmpty()) {
-+ return null;
-+ }
-+ return this.chunkTasks.removeFirst();
-+ }
-+
-+ public void drainTasks() {
-+ ClientChunkTasks task;
-+ while ((task = this.removeFirstTask()) != null) {
-+ task.runTasks();
-+ }
-+ }
-+
-+ public static final class ClientChunkTasks extends ChunkTasks {
-+
-+ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) {
-+ super(chunkCoordinate, lightEngine, queue);
-+ }
-+
-+ @Override
-+ public void run() {
-+ this.runTasks();
-+ }
-+ }
-+ }
-+
-+ public static final class ServerLightQueue extends LightQueue {
-+
-+ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
-+
-+ public ServerLightQueue(final StarLightInterface lightInterface) {
-+ super(lightInterface);
-+ }
-+
-+ public void lowerPriority(final int chunkX, final int chunkZ, final Priority priority) {
-+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (task != null) {
-+ task.lowerPriority(priority);
-+ }
-+ }
-+
-+ public void setPriority(final int chunkX, final int chunkZ, final Priority priority) {
-+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (task != null) {
-+ task.setPriority(priority);
-+ }
-+ }
-+
-+ public void raisePriority(final int chunkX, final int chunkZ, final Priority priority) {
-+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (task != null) {
-+ task.raisePriority(priority);
-+ }
-+ }
-+
-+ public Priority getPriority(final int chunkX, final int chunkZ) {
-+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (task != null) {
-+ return task.getPriority();
-+ }
-+
-+ return Priority.COMPLETING;
-+ }
-+
-+ @Override
-+ public boolean isEmpty() {
-+ return this.chunkTasks.isEmpty();
-+ }
-+
-+ @Override
-+ public ServerChunkTasks queueBlockChange(final BlockPos pos) {
-+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
-+ if (valueInMap == null) {
-+ valueInMap = new ServerChunkTasks(
-+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
-+ );
-+ }
-+ valueInMap.addChangedPosition(pos);
-+ return valueInMap;
-+ });
-+
-+ ret.schedule();
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
-+ if (valueInMap == null) {
-+ valueInMap = new ServerChunkTasks(
-+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
-+ );
-+ }
-+
-+ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
-+
-+ return valueInMap;
-+ });
-+
-+ ret.schedule();
-+
-+ return ret;
-+ }
-+
-+ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final Priority priority) {
-+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
-+ if (valueInMap == null) {
-+ valueInMap = new ServerChunkTasks(
-+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority
-+ );
-+ }
-+
-+ valueInMap.addLightTask(lightTask);
-+
-+ return valueInMap;
-+ });
-+
-+ ret.schedule();
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
-+ if (valueInMap == null) {
-+ valueInMap = new ServerChunkTasks(
-+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
-+ );
-+ }
-+
-+ valueInMap.addEdgeChecksSky(sections);
-+
-+ return valueInMap;
-+ });
-+
-+ ret.schedule();
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
-+ if (valueInMap == null) {
-+ valueInMap = new ServerChunkTasks(
-+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
-+ );
-+ }
-+
-+ valueInMap.addEdgeChecksBlock(sections);
-+
-+ return valueInMap;
-+ });
-+
-+ ret.schedule();
-+
-+ return ret;
-+ }
-+
-+ public static final class ServerChunkTasks extends ChunkTasks {
-+
-+ private final AtomicBoolean ticketAdded = new AtomicBoolean();
-+ private final PrioritisedExecutor.PrioritisedTask task;
-+
-+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
-+ final ServerLightQueue queue) {
-+ this(chunkCoordinate, lightEngine, queue, Priority.NORMAL);
-+ }
-+
-+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
-+ final ServerLightQueue queue, final Priority priority) {
-+ super(chunkCoordinate, lightEngine, queue);
-+ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask(
-+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate),
-+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority
-+ );
-+ }
-+
-+ public boolean markTicketAdded() {
-+ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true);
-+ }
-+
-+ public void schedule() {
-+ this.task.queue();
-+ }
-+
-+ public boolean cancel() {
-+ return this.task.cancel();
-+ }
-+
-+ public Priority getPriority() {
-+ return this.task.getPriority();
-+ }
-+
-+ public void lowerPriority(final Priority priority) {
-+ this.task.lowerPriority(priority);
-+ }
-+
-+ public void setPriority(final Priority priority) {
-+ this.task.setPriority(priority);
-+ }
-+
-+ public void raisePriority(final Priority priority) {
-+ this.task.raisePriority(priority);
-+ }
-+
-+ @Override
-+ public void run() {
-+ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this);
-+
-+ this.runTasks();
-+ }
-+ }
-+ }
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
-@@ -0,0 +1,29 @@
-+package ca.spottedleaf.moonrise.patches.starlight.light;
-+
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.LightLayer;
-+import net.minecraft.world.level.chunk.DataLayer;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import java.util.Collection;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public interface StarLightLightingProvider {
-+
-+ public StarLightInterface starlight$getLightEngine();
-+
-+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
-+ final DataLayer nibble, final boolean trustEdges);
-+
-+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos);
-+
-+ public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk);
-+
-+ public default int starlight$serverRelightChunks(final Collection<ChunkPos> chunks,
-+ final Consumer<ChunkPos> chunkLightCallback,
-+ final IntConsumer onComplete) throws UnsupportedOperationException {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
-@@ -0,0 +1,13 @@
-+package ca.spottedleaf.moonrise.patches.starlight.storage;
-+
-+public interface StarlightSectionData {
-+
-+ public int starlight$getBlockLightState();
-+
-+ public void starlight$setBlockLightState(final int state);
-+
-+ public int starlight$getSkyLightState();
-+
-+ public void starlight$setSkyLightState(final int state);
-+
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742
---- /dev/null
-+++ b/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-@@ -0,0 +1,189 @@
-+package ca.spottedleaf.moonrise.patches.starlight.util;
-+
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
-+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
-+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import org.slf4j.Logger;
-+
-+// note: keep in-sync with SerializableChunkDataMixin
-+public final class SaveUtil {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ public static final int STARLIGHT_LIGHT_VERSION = 9;
-+
-+ public static int getLightVersion() {
-+ return STARLIGHT_LIGHT_VERSION;
-+ }
-+
-+ public static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
-+ public static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
-+ public static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
-+
-+ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
-+ try {
-+ saveLightHookReal(world, chunk, nbt);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
-+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
-+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
-+ }
-+ }
-+
-+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
-+ if (tag == null) {
-+ return;
-+ }
-+
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles();
-+ SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
-+
-+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
-+ // diff start - store our tag for whether light data is init'd
-+ if (lit) {
-+ tag.putBoolean("isLightOn", false);
-+ }
-+ // diff end - store our tag for whether light data is init'd
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+
-+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
-+
-+ ListTag sectionsStored = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sectionsStored.size(); ++i) {
-+ CompoundTag sectionStored = sectionsStored.getCompound(i);
-+ int k = sectionStored.getByte("Y");
-+
-+ // strip light data
-+ sectionStored.remove("BlockLight");
-+ sectionStored.remove("SkyLight");
-+
-+ if (!sectionStored.isEmpty()) {
-+ sections[k - minSection] = sectionStored;
-+ }
-+ }
-+
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
-+ for (int i = minSection; i <= maxSection; ++i) {
-+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
-+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
-+ if (blockNibble != null || skyNibble != null) {
-+ CompoundTag section = sections[i - minSection];
-+ if (section == null) {
-+ section = new CompoundTag();
-+ section.putByte("Y", (byte)i);
-+ sections[i - minSection] = section;
-+ }
-+
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+
-+ if (blockNibble != null) {
-+ if (blockNibble.data != null) {
-+ section.putByteArray("BlockLight", blockNibble.data);
-+ }
-+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
-+ }
-+
-+ if (skyNibble != null) {
-+ if (skyNibble.data != null) {
-+ section.putByteArray("SkyLight", skyNibble.data);
-+ }
-+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
-+ }
-+ }
-+ }
-+ }
-+
-+ // rewrite section list
-+ sectionsStored.clear();
-+ for (CompoundTag section : sections) {
-+ if (section != null) {
-+ sectionsStored.add(section);
-+ }
-+ }
-+ tag.put("sections", sectionsStored);
-+ if (lit) {
-+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
-+ }
-+ }
-+
-+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ try {
-+ loadLightHookReal(world, pos, tag, into);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
-+ // lighting in both cases.
-+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
-+ }
-+ }
-+
-+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ if (into == null) {
-+ return;
-+ }
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ into.setLightCorrect(false); // mark as unlit in case we fail parsing
-+
-+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
-+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
-+
-+
-+ // start copy from the original method
-+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
-+ boolean canReadSky = world.dimensionType().hasSkyLight();
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
-+ ListTag sections = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sections.size(); ++i) {
-+ CompoundTag sectionData = sections.getCompound(i);
-+ int y = sectionData.getByte("Y");
-+
-+ if (sectionData.contains("BlockLight", 7)) {
-+ // this is where our diff is
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
-+ }
-+
-+ if (canReadSky) {
-+ if (sectionData.contains("SkyLight", 7)) {
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
-+ }
-+ }
-+ }
-+ }
-+ // end copy from vanilla
-+
-+ ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles);
-+ ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles);
-+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
-+ }
-+
-+ private SaveUtil() {}
-+}
-diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java
-index a6779295bff446ee79e7c9d41e405447becc2966..efc7f4071655201c59c912e9c84e35a8da66e34c 100644
---- a/io/papermc/paper/FeatureHooks.java
-+++ b/io/papermc/paper/FeatureHooks.java
-@@ -1,6 +1,8 @@
- package io.papermc.paper;
-
- import io.papermc.paper.command.PaperSubcommand;
-+import io.papermc.paper.command.subcommands.ChunkDebugCommand;
-+import io.papermc.paper.command.subcommands.FixLightCommand;
- import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
- import it.unimi.dsi.fastutil.longs.LongSet;
- import it.unimi.dsi.fastutil.longs.LongSets;
-@@ -29,9 +31,12 @@ import org.bukkit.World;
- public final class FeatureHooks {
-
- public static void initChunkTaskScheduler(final boolean useParallelGen) {
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen); // Paper - Chunk system
- }
-
- public static void registerPaperCommands(final Map<Set<String>, PaperSubcommand> commands) {
-+ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system
-+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system
- }
-
- public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
-diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc
---- /dev/null
-+++ b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-@@ -0,0 +1,277 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import ca.spottedleaf.moonrise.common.util.JsonUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
-+import io.papermc.paper.command.CommandUtil;
-+import io.papermc.paper.command.PaperSubcommand;
-+import java.io.File;
-+import java.util.ArrayList;
-+import java.util.Collections;
-+import java.util.List;
-+import java.util.Locale;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.ImposterProtoChunk;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import org.bukkit.Bukkit;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class ChunkDebugCommand implements PaperSubcommand {
-+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> this.doDebug(sender, args);
-+ case "chunkinfo" -> this.doChunkInfo(sender, args);
-+ case "holderinfo" -> this.doHolderInfo(sender, args);
-+ }
-+ return true;
-+ }
-+
-+ @Override
-+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
-+ switch (subCommand) {
-+ case "debug" -> {
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
-+ }
-+ }
-+ case "holderinfo" -> {
-+ List<String> worldNames = new ArrayList<>();
-+ worldNames.add("*");
-+ for (org.bukkit.World world : Bukkit.getWorlds()) {
-+ worldNames.add(world.getName());
-+ }
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
-+ }
-+ }
-+ case "chunkinfo" -> {
-+ List<String> worldNames = new ArrayList<>();
-+ worldNames.add("*");
-+ for (org.bukkit.World world : Bukkit.getWorlds()) {
-+ worldNames.add(world.getName());
-+ }
-+ if (args.length == 1) {
-+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
-+ }
-+ }
-+ }
-+ return Collections.emptyList();
-+ }
-+
-+ private void doChunkInfo(final CommandSender sender, final String[] args) {
-+ List<org.bukkit.World> worlds;
-+ if (args.length < 1 || args[0].equals("*")) {
-+ worlds = Bukkit.getWorlds();
-+ } else {
-+ worlds = new ArrayList<>(args.length);
-+ for (final String arg : args) {
-+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
-+ if (world == null) {
-+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
-+ return;
-+ }
-+ worlds.add(world);
-+ }
-+ }
-+
-+ int accumulatedTotal = 0;
-+ int accumulatedInactive = 0;
-+ int accumulatedBorder = 0;
-+ int accumulatedTicking = 0;
-+ int accumulatedEntityTicking = 0;
-+
-+ for (final org.bukkit.World bukkitWorld : worlds) {
-+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
-+
-+ int total = 0;
-+ int inactive = 0;
-+ int full = 0;
-+ int blockTicking = 0;
-+ int entityTicking = 0;
-+
-+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
-+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
-+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
-+
-+ if (!(chunk instanceof LevelChunk fullChunk)) {
-+ continue;
-+ }
-+
-+ ++total;
-+
-+ switch (holder.getChunkStatus()) {
-+ case INACCESSIBLE: {
-+ ++inactive;
-+ break;
-+ }
-+ case FULL: {
-+ ++full;
-+ break;
-+ }
-+ case BLOCK_TICKING: {
-+ ++blockTicking;
-+ break;
-+ }
-+ case ENTITY_TICKING: {
-+ ++entityTicking;
-+ break;
-+ }
-+ }
-+ }
-+
-+ accumulatedTotal += total;
-+ accumulatedInactive += inactive;
-+ accumulatedBorder += full;
-+ accumulatedTicking += blockTicking;
-+ accumulatedEntityTicking += entityTicking;
-+
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(total),
-+ text(" Inactive: ", BLUE), text(inactive),
-+ text(" Full: ", BLUE), text(full),
-+ text(" Block Ticking: ", BLUE), text(blockTicking),
-+ text(" Entity Ticking: ", BLUE), text(entityTicking)
-+ ));
-+ }
-+ if (worlds.size() > 1) {
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(accumulatedTotal),
-+ text(" Inactive: ", BLUE), text(accumulatedInactive),
-+ text(" Full: ", BLUE), text(accumulatedBorder),
-+ text(" Block Ticking: ", BLUE), text(accumulatedTicking),
-+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking)
-+ ));
-+ }
-+ }
-+
-+ private void doHolderInfo(final CommandSender sender, final String[] args) {
-+ List<org.bukkit.World> worlds;
-+ if (args.length < 1 || args[0].equals("*")) {
-+ worlds = Bukkit.getWorlds();
-+ } else {
-+ worlds = new ArrayList<>(args.length);
-+ for (final String arg : args) {
-+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
-+ if (world == null) {
-+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
-+ return;
-+ }
-+ worlds.add(world);
-+ }
-+ }
-+
-+ int accumulatedTotal = 0;
-+ int accumulatedCanUnload = 0;
-+ int accumulatedNull = 0;
-+ int accumulatedReadOnly = 0;
-+ int accumulatedProtoChunk = 0;
-+ int accumulatedFullChunk = 0;
-+
-+ for (final org.bukkit.World bukkitWorld : worlds) {
-+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
-+
-+ int total = 0;
-+ int canUnload = 0;
-+ int nullChunks = 0;
-+ int readOnly = 0;
-+ int protoChunk = 0;
-+ int fullChunk = 0;
-+
-+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
-+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
-+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
-+
-+ ++total;
-+
-+ if (chunk == null) {
-+ ++nullChunks;
-+ } else if (chunk instanceof ImposterProtoChunk) {
-+ ++readOnly;
-+ } else if (chunk instanceof ProtoChunk) {
-+ ++protoChunk;
-+ } else if (chunk instanceof LevelChunk) {
-+ ++fullChunk;
-+ }
-+
-+ if (holder.isSafeToUnload() == null) {
-+ ++canUnload;
-+ }
-+ }
-+
-+ accumulatedTotal += total;
-+ accumulatedCanUnload += canUnload;
-+ accumulatedNull += nullChunks;
-+ accumulatedReadOnly += readOnly;
-+ accumulatedProtoChunk += protoChunk;
-+ accumulatedFullChunk += fullChunk;
-+
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(total),
-+ text(" Unloadable: ", BLUE), text(canUnload),
-+ text(" Null: ", BLUE), text(nullChunks),
-+ text(" ReadOnly: ", BLUE), text(readOnly),
-+ text(" Proto: ", BLUE), text(protoChunk),
-+ text(" Full: ", BLUE), text(fullChunk)
-+ ));
-+ }
-+ if (worlds.size() > 1) {
-+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
-+ sender.sendMessage(text().color(DARK_AQUA).append(
-+ text("Total: ", BLUE), text(accumulatedTotal),
-+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload),
-+ text(" Null: ", BLUE), text(accumulatedNull),
-+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly),
-+ text(" Proto: ", BLUE), text(accumulatedProtoChunk),
-+ text(" Full: ", BLUE), text(accumulatedFullChunk)
-+ ));
-+ }
-+ }
-+
-+ private void doDebug(final CommandSender sender, final String[] args) {
-+ if (args.length < 1) {
-+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
-+ return;
-+ }
-+
-+ final String debugType = args[0].toLowerCase(Locale.ROOT);
-+ switch (debugType) {
-+ case "chunks" -> {
-+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) {
-+ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED));
-+ break;
-+ }
-+ final File file = ChunkTaskScheduler.getChunkDebugFile();
-+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN));
-+ try {
-+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file);
-+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
-+ } catch (Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
-+ }
-+ }
-+ // "help" & default
-+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
-+ }
-+ }
-+
-+}
-diff --git a/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e
---- /dev/null
-+++ b/io/papermc/paper/command/subcommands/FixLightCommand.java
-@@ -0,0 +1,116 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
-+import io.papermc.paper.command.PaperSubcommand;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ThreadedLevelLightEngine;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.entity.Player;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import java.text.DecimalFormat;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class FixLightCommand implements PaperSubcommand {
-+
-+ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
-+ return new DecimalFormat("#,##0.0");
-+ });
-+
-+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ this.doFixLight(sender, args);
-+ return true;
-+ }
-+
-+ private void doFixLight(final CommandSender sender, final String[] args) {
-+ if (!(sender instanceof Player)) {
-+ sender.sendMessage(text("Only players can use this command", RED));
-+ return;
-+ }
-+ @Nullable Runnable post = null;
-+ int radius = 2;
-+ if (args.length > 0) {
-+ try {
-+ final int parsed = Integer.parseInt(args[0]);
-+ if (parsed < 0) {
-+ sender.sendMessage(text("Radius cannot be negative!", RED));
-+ return;
-+ }
-+ final int maxRadius = 32;
-+ radius = Math.min(maxRadius, parsed);
-+ if (radius != parsed) {
-+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED));
-+ }
-+ } catch (final Exception e) {
-+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED));
-+ return;
-+ }
-+ }
-+
-+ CraftPlayer player = (CraftPlayer) sender;
-+ ServerPlayer handle = player.getHandle();
-+ ServerLevel world = (ServerLevel) handle.level();
-+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
-+ this.starlightFixLight(handle, world, lightengine, radius, post);
-+ }
-+
-+ private void starlightFixLight(
-+ final ServerPlayer sender,
-+ final ServerLevel world,
-+ final ThreadedLevelLightEngine lightengine,
-+ final int radius,
-+ final @Nullable Runnable done
-+ ) {
-+ final long start = System.nanoTime();
-+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
-+
-+ final int[] pending = new int[1];
-+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ ++pending[0];
-+ }
-+
-+ final int[] relitChunks = new int[1];
-+ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks,
-+ (final ChunkPos chunkPos) -> {
-+ ++relitChunks[0];
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit chunk ", BLUE), text(chunkPos.toString()),
-+ text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%")
-+ ));
-+ },
-+ (final int totalRelit) -> {
-+ final long end = System.nanoTime();
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit ", BLUE), text(totalRelit),
-+ text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms")
-+ ));
-+ if (done != null) {
-+ done.run();
-+ }
-+ }
-+ );
-+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/TickRegions.java
-@@ -0,0 +1,10 @@
-+package io.papermc.paper.threadedregions;
-+
-+// placeholder class for Folia
-+public class TickRegions {
-+
-+ public static int getRegionChunkShift() {
-+ return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT;
-+ }
-+
-+}
-diff --git a/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java
-index 690e1d2394e68356c56a39ac083cc53ee0388d71..928f38fd6beb00753c92ae9f4678f7507519a39b 100644
---- a/net/minecraft/core/Direction.java
-+++ b/net/minecraft/core/Direction.java
-@@ -28,7 +28,7 @@ import org.joml.Quaternionf;
- import org.joml.Vector3f;
- import org.joml.Vector4f;
-
--public enum Direction implements StringRepresentable {
-+public enum Direction implements StringRepresentable, ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection { // Paper - optimise collisions
- DOWN(0, 1, -1, "down", Direction.AxisDirection.NEGATIVE, Direction.Axis.Y, new Vec3i(0, -1, 0)),
- UP(1, 0, -1, "up", Direction.AxisDirection.POSITIVE, Direction.Axis.Y, new Vec3i(0, 1, 0)),
- NORTH(2, 3, 2, "north", Direction.AxisDirection.NEGATIVE, Direction.Axis.Z, new Vec3i(0, 0, -1)),
-@@ -62,6 +62,46 @@ public enum Direction implements StringRepresentable {
- private final int adjY;
- private final int adjZ;
- // Paper end - Perf: Inline shift direction fields
-+ // Paper start - optimise collisions
-+ private static final int RANDOM_OFFSET = 2017601568;
-+ private Direction opposite;
-+ private Quaternionf rotation;
-+ private int id;
-+ private int stepX;
-+ private int stepY;
-+ private int stepZ;
-+
-+ private Quaternionf getRotationUncached() {
-+ switch ((Direction)(Object)this) {
-+ case DOWN: {
-+ return new Quaternionf().rotationX(3.1415927F);
-+ }
-+ case UP: {
-+ return new Quaternionf();
-+ }
-+ case NORTH: {
-+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 3.1415927F);
-+ }
-+ case SOUTH: {
-+ return new Quaternionf().rotationX(1.5707964F);
-+ }
-+ case WEST: {
-+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 1.5707964F);
-+ }
-+ case EAST: {
-+ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, -1.5707964F);
-+ }
-+ default: {
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final int moonrise$uniqueId() {
-+ return this.id;
-+ }
-+ // Paper end - optimise collisions
-
- private Direction(
- final int id,
-@@ -147,14 +187,13 @@ public enum Direction implements StringRepresentable {
- }
-
- public Quaternionf getRotation() {
-- return switch (this) {
-- case DOWN -> new Quaternionf().rotationX((float) Math.PI);
-- case UP -> new Quaternionf();
-- case NORTH -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) Math.PI);
-- case SOUTH -> new Quaternionf().rotationX((float) (Math.PI / 2));
-- case WEST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (Math.PI / 2));
-- case EAST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (-Math.PI / 2));
-- };
-+ // Paper start - optimise collisions
-+ try {
-+ return (Quaternionf)this.rotation.clone();
-+ } catch (final CloneNotSupportedException ex) {
-+ throw new InternalError(ex);
-+ }
-+ // Paper end - optimise collisions
- }
-
- public int get3DDataValue() {
-@@ -178,7 +217,7 @@ public enum Direction implements StringRepresentable {
- }
-
- public Direction getOpposite() {
-- return from3DDataValue(this.oppositeIndex);
-+ return this.opposite; // Paper - optimise collisions
- }
-
- public Direction getClockWise(Direction.Axis axis) {
-@@ -600,4 +639,17 @@ public enum Direction implements StringRepresentable {
- return this.faces.length;
- }
- }
-+
-+ // Paper start - optimise collisions
-+ static {
-+ for (final Direction direction : VALUES) {
-+ ((Direction)(Object)direction).opposite = from3DDataValue(((Direction)(Object)direction).oppositeIndex);
-+ ((Direction)(Object)direction).rotation = ((Direction)(Object)direction).getRotationUncached();
-+ ((Direction)(Object)direction).id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(direction.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+ ((Direction)(Object)direction).stepX = ((Direction)(Object)direction).normal.getX();
-+ ((Direction)(Object)direction).stepY = ((Direction)(Object)direction).normal.getY();
-+ ((Direction)(Object)direction).stepZ = ((Direction)(Object)direction).normal.getZ();
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-diff --git a/net/minecraft/core/MappedRegistry.java b/net/minecraft/core/MappedRegistry.java
-index 063630c1ffcce099139c59d598fc5a210e21f640..a61153c5d99bdc26f37a10f33baf839e943e17e1 100644
---- a/net/minecraft/core/MappedRegistry.java
-+++ b/net/minecraft/core/MappedRegistry.java
-@@ -50,6 +50,19 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
- return this.getTags();
- }
-
-+ // Paper start - fluid method optimisations
-+ private void injectFluidRegister(
-+ final ResourceKey<?> resourceKey,
-+ final T object
-+ ) {
-+ if (resourceKey.registryKey() == (Object)net.minecraft.core.registries.Registries.FLUID) {
-+ for (final net.minecraft.world.level.material.FluidState possibleState : ((net.minecraft.world.level.material.Fluid)object).getStateDefinition().getPossibleStates()) {
-+ ((ca.spottedleaf.moonrise.patches.fluid.FluidFluidState)(Object)possibleState).moonrise$initCaches();
-+ }
-+ }
-+ }
-+ // Paper end - fluid method optimisations
-+
- public MappedRegistry(ResourceKey<? extends Registry<T>> key, Lifecycle lifecycle) {
- this(key, lifecycle, false);
- }
-@@ -114,6 +127,7 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
- this.toId.put(value, i);
- this.registrationInfos.put(key, info);
- this.registryLifecycle = this.registryLifecycle.add(info.lifecycle());
-+ this.injectFluidRegister(key, value); // Paper - fluid method optimisations
- return reference;
- }
- }
-diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
-index 731bdabd53fd4a3d17494f26781223097a5d6e16..42d46c7a7437bea5335a23cbee5708ac57131474 100644
---- a/net/minecraft/server/Main.java
-+++ b/net/minecraft/server/Main.java
-@@ -322,6 +322,7 @@ public class Main {
-
- convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
- */
-+ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async
- final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
- DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
-
-diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
-index 807d05097f7313361eadb600187421d25e294413..5e7ba47247fc9b6bc8da86d8f67c6cd923cd0b1e 100644
---- a/net/minecraft/server/MinecraftServer.java
-+++ b/net/minecraft/server/MinecraftServer.java
-@@ -204,7 +204,7 @@ import org.bukkit.event.server.ServerLoadEvent;
- // CraftBukkit end
-
-
--public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource {
-+public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system
-
- private static MinecraftServer SERVER; // Paper
- public static final Logger LOGGER = LogUtils.getLogger();
-@@ -333,7 +333,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
- ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
- AtomicReference<S> atomicreference = new AtomicReference();
-- Thread thread = new Thread(() -> {
-+ Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system
- ((MinecraftServer) atomicreference.get()).runServer();
- }, "Server thread");
-
-@@ -352,6 +352,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- return s0;
- }
-
-+ // Paper start - rewrite chunk system
-+ private volatile Throwable chunkSystemCrash;
-+
-+ @Override
-+ public final void moonrise$setChunkSystemCrash(final Throwable throwable) {
-+ this.chunkSystemCrash = throwable;
-+ }
-+
-+ private static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us
-+ private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
-+ private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
-+
-+ private long lastMidTickExecute;
-+ private long lastMidTickExecuteFailure;
-+
-+ private boolean tickMidTickTasks() {
-+ // give all worlds a fair chance at by targeting them all.
-+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
-+ boolean executed = false;
-+ for (final ServerLevel world : this.getAllLevels()) {
-+ long currTime = System.nanoTime();
-+ if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) {
-+ continue;
-+ }
-+ if (!world.getChunkSource().pollTask()) {
-+ // we need to back off if this fails
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime);
-+ } else {
-+ executed = true;
-+ }
-+ }
-+
-+ return executed;
-+ }
-+
-+ @Override
-+ public final void moonrise$executeMidTickTasks() {
-+ final long startTime = System.nanoTime();
-+ if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
-+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
-+ // so, backoff to prevent this
-+ return;
-+ }
-+
-+ for (;;) {
-+ final boolean moreTasks = this.tickMidTickTasks();
-+ final long currTime = System.nanoTime();
-+ final long diff = currTime - startTime;
-+
-+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
-+ if (!moreTasks) {
-+ this.lastMidTickExecuteFailure = currTime;
-+ }
-+
-+ // note: negative values reduce the time
-+ long overuse = diff - MAX_CHUNK_EXEC_TIME;
-+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms
-+ // make sure something like a GC or dumb plugin doesn't screw us over...
-+ overuse = 10L * 1000L * 1000L; // 10ms
-+ }
-+
-+ final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
-+ final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
-+
-+ this.lastMidTickExecute = currTime + extraSleep;
-+ return;
-+ }
-+ }
-+ }
-+ // Paper end - rewrite chunk system
-+
- public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
- super("Server");
- SERVER = this; // Paper - better singleton
-@@ -672,7 +743,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- this.forceDifficulty();
- for (ServerLevel worldserver : this.getAllLevels()) {
- this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
-- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
-+ // Paper - rewrite chunk system
- this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
- }
-
-@@ -888,6 +959,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- public abstract boolean shouldRconBroadcast();
-
- public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) {
-+ // Paper start - add close param
-+ return this.saveAllChunks(suppressLogs, flush, force, false);
-+ }
-+ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) {
-+ // Paper end - add close param
- boolean flag3 = false;
-
- for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
-@@ -897,7 +973,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
- }
-
-- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
-+ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param
- }
-
- // CraftBukkit start - moved to WorldServer.save
-@@ -998,7 +1074,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- }
- }
-
-- while (this.levels.values().stream().anyMatch((worldserver1) -> {
-+ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system
- return worldserver1.getChunkSource().chunkMap.hasWork();
- })) {
- this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
-@@ -1015,19 +1091,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- this.waitUntilNextTick();
- }
-
-- this.saveAllChunks(false, true, false);
-- iterator = this.getAllLevels().iterator();
--
-- while (iterator.hasNext()) {
-- worldserver = (ServerLevel) iterator.next();
-- if (worldserver != null) {
-- try {
-- worldserver.close();
-- } catch (IOException ioexception) {
-- MinecraftServer.LOGGER.error("Exception closing the level", ioexception);
-- }
-- }
-- }
-+ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system
-
- this.isSaving = false;
- this.resources.close();
-@@ -1047,6 +1111,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- }
- // Spigot end
-
-+ // Paper start - rewrite chunk system
-+ LOGGER.info("Waiting for I/O tasks to complete...");
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush((MinecraftServer)(Object)this);
-+ LOGGER.info("All I/O tasks to complete");
-+ if ((Object)this instanceof DedicatedServer) {
-+ ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors();
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- public String getLocalIp() {
-@@ -1228,6 +1300,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- this.tickServer(flag ? () -> {
- return false;
- } : this::haveTime);
-+ // Paper start - rewrite chunk system
-+ final Throwable crash = this.chunkSystemCrash;
-+ if (crash != null) {
-+ this.chunkSystemCrash = null;
-+ throw new RuntimeException("Chunk system crash propagated to tick()", crash);
-+ }
-+ // Paper end - rewrite chunk system
- this.tickFrame.end();
- gameprofilerfiller.popPush("nextTickWait");
- this.mayHaveDelayedTasks = true;
-@@ -1432,6 +1511,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-
- private boolean pollTaskInternal() {
- if (super.pollTask()) {
-+ this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system
- return true;
- } else {
- boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
-@@ -2713,6 +2793,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-
- }
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public boolean isSameThread() {
-+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread();
-+ }
-+ // Paper end - rewrite chunk system
-+
- // CraftBukkit start
- public boolean isDebugging() {
- return false;
-diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
-index 2f47d95943c00020a24ea3ff1a49e64e114de675..0dd9ed7465d222505d5368781654ec4954f6e5c3 100644
---- a/net/minecraft/server/dedicated/DedicatedServer.java
-+++ b/net/minecraft/server/dedicated/DedicatedServer.java
-@@ -458,7 +458,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
- return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true;
- }
-
-+ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system
-+
- public void handleConsoleInput(String command, CommandSourceStack commandSource) {
-+ // Paper start - rewrite chunk system
-+ if (command.equalsIgnoreCase("paper debug chunks --async")) {
-+ LOGGER.info("Scheduling async debug chunks");
-+ Runnable run = () -> {
-+ LOGGER.info("Async debug chunks executing");
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false);
-+ CommandSender sender = MinecraftServer.getServer().console;
-+ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile();
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN));
-+ try {
-+ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file);
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN));
-+ } catch (Throwable thr) {
-+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
-+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED));
-+ }
-+ };
-+ Thread t = new Thread(run);
-+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement());
-+ t.setDaemon(true);
-+ t.start();
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
- this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
- }
-
-diff --git a/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java
-index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8dd9375f2ad2c65a773a3195aeff1f977e09e7e0 100644
---- a/net/minecraft/server/level/ChunkHolder.java
-+++ b/net/minecraft/server/level/ChunkHolder.java
-@@ -32,46 +32,125 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
- import net.minecraft.server.MinecraftServer;
- // CraftBukkit end
-
--public class ChunkHolder extends GenerationChunkHolder {
-+public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system
-
- public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
- private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
- private final LevelHeightAccessor levelHeightAccessor;
-- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
-- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
-- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
-- public int oldTicketLevel;
-- private int ticketLevel;
-- private int queueLevel;
-+ // Paper - rewrite chunk system
- private boolean hasChangedSections;
- private final ShortSet[] changedBlocksPerSection;
- private final BitSet blockChangedLightSectionFilter;
- private final BitSet skyChangedLightSectionFilter;
- private final LevelLightEngine lightEngine;
-- private final ChunkHolder.LevelChangeListener onLevelChange;
-+ // Paper - rewrite chunk system
- public final ChunkHolder.PlayerProvider playerProvider;
-- private boolean wasAccessibleSinceLastSave;
-- private CompletableFuture<?> pendingFullStateConfirmation;
-- private CompletableFuture<?> sendSync;
-- private CompletableFuture<?> saveSync;
-+ // Paper - rewrite chunk system
-+
-+ // Paper start - rewrite chunk system
-+ 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);
-+
-+ private ChunkMap getChunkMap() {
-+ return (ChunkMap)this.playerProvider;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() {
-+ return this.newChunkHolder;
-+ }
-+
-+ @Override
-+ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) {
-+ this.newChunkHolder = newChunkHolder;
-+ }
-+
-+ @Override
-+ public final void moonrise$addReceivedChunk(final ServerPlayer player) {
-+ if (!this.playersSentChunkTo.add(player)) {
-+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$removeReceivedChunk(final ServerPlayer player) {
-+ if (!this.playersSentChunkTo.remove(player)) {
-+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
-+ }
-+ }
-+
-+ @Override
-+ public final boolean moonrise$hasChunkBeenSent() {
-+ return this.playersSentChunkTo.size() != 0;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) {
-+ return this.playersSentChunkTo.contains(to);
-+ }
-+
-+ @Override
-+ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) {
-+ final List<ServerPlayer> ret = new java.util.ArrayList<>();
-+ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked();
-+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
-+ final ServerPlayer player = raw[i];
-+ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
-+ continue;
-+ }
-+ ret.add(player);
-+ }
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public final LevelChunk moonrise$getFullChunk() {
-+ if (this.newChunkHolder.isFullChunkReady()) {
-+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
-+ return levelChunk;
-+ } // else: race condition: chunk unload
-+ }
-+ return null;
-+ }
-+
-+ private boolean isRadiusLoaded(final int radius) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler()
-+ .chunkHolderManager;
-+ final ChunkPos pos = this.pos;
-+ final int chunkX = pos.x;
-+ final int chunkZ = pos.z;
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ);
-+
-+ if (holder == null || !holder.isFullChunkReady()) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+ // Paper end - rewrite chunk system
-
- public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
- super(pos);
-- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-+ // Paper - rewrite chunk system
- this.blockChangedLightSectionFilter = new BitSet();
- this.skyChangedLightSectionFilter = new BitSet();
-- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
-+ // Paper - rewrite chunk system
- this.levelHeightAccessor = world;
- this.lightEngine = lightingProvider;
-- this.onLevelChange = levelUpdateListener;
-+ // Paper - rewrite chunk system
- this.playerProvider = playersWatchingChunkProvider;
-- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
-- this.ticketLevel = this.oldTicketLevel;
-- this.queueLevel = this.oldTicketLevel;
-+ // Paper - rewrite chunk system
- this.setTicketLevel(level);
- this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
- }
-@@ -79,7 +158,7 @@ public class ChunkHolder extends GenerationChunkHolder {
- // CraftBukkit start
- public LevelChunk getFullChunkNow() {
- // Note: We use the oldTicketLevel for isLoaded checks.
-- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
-+ if (!this.newChunkHolder.isFullChunkReady()) return null; // Paper - rewrite chunk system
- return this.getFullChunkNowUnchecked();
- }
-
-@@ -89,64 +168,65 @@ public class ChunkHolder extends GenerationChunkHolder {
- // CraftBukkit end
-
- public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
-- return this.tickingChunkFuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() {
-- return this.entityTickingChunkFuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() {
-- return this.fullChunkFuture;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
- public final LevelChunk getTickingChunk() { // Paper - final for inline
-- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
-+ // Paper start - rewrite chunk system
-+ if (this.newChunkHolder.isTickingReady()) {
-+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
-+ return levelChunk;
-+ } // else: race condition: chunk unload
-+ }
-+ return null;
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- public LevelChunk getChunkToSend() {
-- return !this.sendSync.isDone() ? null : this.getTickingChunk();
-+ // Paper start - rewrite chunk system
-+ final LevelChunk ret = this.moonrise$getFullChunk();
-+ if (ret != null && this.isRadiusLoaded(1)) {
-+ return ret;
-+ }
-+ return null;
-+ // Paper end - rewrite chunk system
- }
-
- public CompletableFuture<?> getSendSyncFuture() {
-- return this.sendSync;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
-- if (this.sendSync.isDone()) {
-- this.sendSync = postProcessingFuture;
-- } else {
-- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
-- return null;
-- });
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-
- }
-
- public CompletableFuture<?> getSaveSyncFuture() {
-- return this.saveSync;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public boolean isReadyForSaving() {
-- return this.saveSync.isDone();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- protected void addSaveDependency(CompletableFuture<?> savingFuture) {
-- if (this.saveSync.isDone()) {
-- this.saveSync = savingFuture;
-- } else {
-- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> {
-- return null;
-- });
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-
- }
-
- public boolean blockChanged(BlockPos pos) {
-- LevelChunk chunk = this.getTickingChunk();
-+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
-
- if (chunk == null) {
- return false;
-@@ -172,7 +252,7 @@ public class ChunkHolder extends GenerationChunkHolder {
- return false;
- } else {
- ichunkaccess.markUnsaved();
-- LevelChunk chunk = this.getTickingChunk();
-+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
-
- if (chunk == null) {
- return false;
-@@ -207,7 +287,7 @@ public class ChunkHolder extends GenerationChunkHolder {
- List list;
-
- if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
-- list = this.playerProvider.getPlayers(this.pos, true);
-+ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
- if (!list.isEmpty()) {
- ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter);
-
-@@ -219,7 +299,7 @@ public class ChunkHolder extends GenerationChunkHolder {
- }
-
- if (this.hasChangedSections) {
-- list = this.playerProvider.getPlayers(this.pos, false);
-+ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
-
- for (int i = 0; i < this.changedBlocksPerSection.length; ++i) {
- ShortSet shortset = this.changedBlocksPerSection[i];
-@@ -285,201 +365,48 @@ public class ChunkHolder extends GenerationChunkHolder {
-
- @Override
- public int getTicketLevel() {
-- return this.ticketLevel;
-+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system
- }
-
- @Override
- public int getQueueLevel() {
-- return this.queueLevel;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void setQueueLevel(int level) {
-- this.queueLevel = level;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void setTicketLevel(int level) {
-- this.ticketLevel = level;
-+ // Paper - rewrite chunk system
- }
-
- private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) {
-- this.pendingFullStateConfirmation.cancel(false);
-- CompletableFuture<Void> completablefuture1 = new CompletableFuture();
--
-- completablefuture1.thenRunAsync(() -> {
-- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
-- }, executor);
-- this.pendingFullStateConfirmation = completablefuture1;
-- chunkFuture.thenAccept((chunkresult) -> {
-- chunkresult.ifSuccess((chunk) -> {
-- completablefuture1.complete(null); // CraftBukkit - decompile error
-- });
-- });
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) {
-- this.pendingFullStateConfirmation.cancel(false);
-- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- // CraftBukkit start
- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
- // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks
- protected void callEventIfUnloading(ChunkMap playerchunkmap) {
-- FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
-- FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel);
-- boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
-- boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
-- if (oldIsFull && !newIsFull) {
-- this.getFullChunkFuture().thenAccept((either) -> {
-- LevelChunk chunk = (LevelChunk) either.orElse(null);
-- if (chunk != null) {
-- playerchunkmap.callbackExecutor.execute(() -> {
-- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
-- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
-- // These actions may however happen deferred, so we manually set the needsSaving flag already here.
-- chunk.markUnsaved();
-- chunk.unloadCallback();
-- });
-- }
-- }).exceptionally((throwable) -> {
-- // ensure exceptions are printed, by default this is not the case
-- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
-- return null;
-- });
--
-- // Run callback right away if the future was already done
-- playerchunkmap.callbackExecutor.run();
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
- // CraftBukkit end
-
- protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
-- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
-- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
-- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
-- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
--
-- this.wasAccessibleSinceLastSave |= flag1;
-- if (!flag && flag1) {
-- int expectCreateCount = ++this.fullChunkCreateCount; // Paper
-- this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
-- this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
-- // Paper start - cache ticking ready status
-- this.fullChunkFuture.thenAccept(chunkResult -> {
-- chunkResult.ifSuccess(chunk -> {
-- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
-- ChunkHolder.this.isFullChunkReady = true;
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
-- }
-- });
-- });
-- // Paper end - cache ticking ready status
-- this.addSaveDependency(this.fullChunkFuture);
-- }
--
-- if (flag && !flag1) {
-- // Paper start
-- if (this.isFullChunkReady) {
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
-- }
-- // Paper end
-- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
-- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- }
--
-- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
-- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
--
-- if (!flag2 && flag3) {
-- this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
-- this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
-- // Paper start - cache ticking ready status
-- this.tickingChunkFuture.thenAccept(chunkResult -> {
-- chunkResult.ifSuccess(chunk -> {
-- // note: Here is a very good place to add callbacks to logic waiting on this.
-- ChunkHolder.this.isTickingReady = true;
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
-- });
-- });
-- // Paper end
-- this.addSaveDependency(this.tickingChunkFuture);
-- }
--
-- if (flag2 && !flag3) {
-- // Paper start
-- if (this.isTickingReady) {
-- 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
-- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- }
--
-- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
-- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
--
-- if (!flag4 && flag5) {
-- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
-- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException());
-- }
--
-- this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
-- this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
-- // Paper start - cache ticking ready status
-- this.entityTickingChunkFuture.thenAccept(chunkResult -> {
-- chunkResult.ifSuccess(chunk -> {
-- ChunkHolder.this.isEntityTickingReady = true;
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
-- });
-- });
-- // Paper end
-- this.addSaveDependency(this.entityTickingChunkFuture);
-- }
--
-- if (flag4 && !flag5) {
-- // Paper start
-- if (this.isEntityTickingReady) {
-- 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
-- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-- }
--
-- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) {
-- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1);
-- }
--
-- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
-- this.oldTicketLevel = this.ticketLevel;
-- // CraftBukkit start
-- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
-- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
-- this.getFullChunkFuture().thenAccept((either) -> {
-- LevelChunk chunk = (LevelChunk) either.orElse(null);
-- if (chunk != null) {
-- chunkLoadingManager.callbackExecutor.execute(() -> {
-- chunk.loadCallback();
-- });
-- }
-- }).exceptionally((throwable) -> {
-- // ensure exceptions are printed, by default this is not the case
-- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
-- return null;
-- });
--
-- // Run callback right away if the future was already done
-- chunkLoadingManager.callbackExecutor.run();
-- }
-- // CraftBukkit end
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public boolean wasAccessibleSinceLastSave() {
-- return this.wasAccessibleSinceLastSave;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void refreshAccessibility() {
-- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @FunctionalInterface
-diff --git a/net/minecraft/server/level/ChunkLevel.java b/net/minecraft/server/level/ChunkLevel.java
-index 11b30b6daa1d049634350e34502c701e9800add4..fae17a075d7efaf24d916877dd5968eb9652bb66 100644
---- a/net/minecraft/server/level/ChunkLevel.java
-+++ b/net/minecraft/server/level/ChunkLevel.java
-@@ -7,8 +7,8 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
- import org.jetbrains.annotations.Contract;
-
- public class ChunkLevel {
-- private static final int FULL_CHUNK_LEVEL = 33;
-- private static final int BLOCK_TICKING_LEVEL = 32;
-+ public static final int FULL_CHUNK_LEVEL = 33;
-+ public static final int BLOCK_TICKING_LEVEL = 32;
- public static final int ENTITY_TICKING_LEVEL = 31;
- private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
- public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
-diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
-index e9b585387f6cbc454e7b16feb36a256e733c5488..67cfc3236a39008cfcf3acffefafda1a604b8573 100644
---- a/net/minecraft/server/level/ChunkMap.java
-+++ b/net/minecraft/server/level/ChunkMap.java
-@@ -108,7 +108,7 @@ import org.slf4j.Logger;
- import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
- // CraftBukkit end
-
--public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap {
-+public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider, GeneratingChunkMap, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemChunkMap { // Paper - rewrite chunk system
-
- private static final ChunkResult<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range");
- private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(ChunkMap.UNLOADED_CHUNK_LIST_RESULT);
-@@ -123,10 +123,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- public static final int MIN_VIEW_DISTANCE = 2;
- public static final int MAX_VIEW_DISTANCE = 32;
- public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
-- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
-- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
-- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
-- private final List<ChunkGenerationTask> pendingGenerationTasks;
-+ // Paper - rewrite chunk system
- public final ServerLevel level;
- private final ThreadedLevelLightEngine lightEngine;
- private final BlockableEventLoop<Runnable> mainThreadExecutor;
-@@ -136,22 +133,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private final PoiManager poiManager;
- public final LongSet toDrop;
- private boolean modified;
-- private final ChunkTaskDispatcher worldgenTaskDispatcher;
-- private final ChunkTaskDispatcher lightTaskDispatcher;
-+ // Paper - rewrite chunk system
- public final ChunkProgressListener progressListener;
- private final ChunkStatusUpdateListener chunkStatusListener;
- public final ChunkMap.ChunkDistanceManager distanceManager;
-- private final AtomicInteger tickingGenerated;
-+ public final AtomicInteger tickingGenerated; // Paper - public
- private final String storageName;
- private final PlayerMap playerMap;
- public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
- private final Long2ByteMap chunkTypeCache;
-- private final Long2LongMap nextChunkSaveTime;
-- private final LongSet chunksToEagerlySave;
-- private final Queue<Runnable> unloadQueue;
-- private final AtomicInteger activeChunkWrites;
-+ // Paper - rewrite chunk system
- public int serverViewDistance;
-- private final WorldGenContext worldGenContext;
-+ public final WorldGenContext worldGenContext; // Paper - public
-
- // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
- public final CallbackExecutor callbackExecutor = new CallbackExecutor();
-@@ -176,24 +169,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- // Paper start
- public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
-- return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ return null; // Paper - rewrite chunk system
- }
- // Paper end
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final void moonrise$writeFinishCallback(final ChunkPos pos) throws IOException {
-+ // see ChunkStorage#write
-+ this.handleLegacyStructureIndex(pos);
-+ }
-+ // Paper end - rewrite chunk system
-
- 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();
-- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
-- this.pendingGenerationTasks = new ArrayList();
-+ // Paper - rewrite chunk system
- this.toDrop = new LongOpenHashSet();
- this.tickingGenerated = new AtomicInteger();
- this.playerMap = new PlayerMap();
- this.entityMap = new Int2ObjectOpenHashMap();
- this.chunkTypeCache = new Long2ByteOpenHashMap();
-- this.nextChunkSaveTime = new Long2LongOpenHashMap();
-- this.chunksToEagerlySave = new LongLinkedOpenHashSet();
-- this.unloadQueue = Queues.newConcurrentLinkedQueue();
-- this.activeChunkWrites = new AtomicInteger();
-+ // Paper - rewrite chunk system
- Path path = session.getDimensionPath(world.dimension());
-
- this.storageName = path.getFileName().toString();
-@@ -221,18 +216,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.chunkStatusListener = chunkStatusChangeListener;
- ConsecutiveExecutor consecutiveexecutor1 = new ConsecutiveExecutor(executor, "light");
-
-- this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor, executor);
-- this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveexecutor1, executor);
-- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, this.lightTaskDispatcher);
-+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), consecutiveexecutor1, null); // Paper - rewrite chunk system
- this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
- this.overworldDataStorage = persistentStateManagerFactory;
- this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
- this.setServerViewDistance(viewDistance);
-- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved);
-+ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system
- }
-
- private void setChunkUnsaved(ChunkPos pos) {
-- this.chunksToEagerlySave.add(pos.toLong());
-+ // Paper - rewrite chunk system
- }
-
- // Paper start
-@@ -263,23 +256,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
-- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system
- }
-
- private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
-- if (!this.isChunkTracked(player, chunkX, chunkZ)) {
-- return false;
-- } else {
-- for (int k = -1; k <= 1; ++k) {
-- for (int l = -1; l <= 1; ++l) {
-- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) {
-- return true;
-- }
-- }
-- }
--
-- return false;
-- }
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system
- }
-
- protected ThreadedLevelLightEngine getLightEngine() {
-@@ -288,20 +269,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- @Nullable
- protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
-- return (ChunkHolder) this.updatingChunkMap.get(pos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
-+ return holder == null ? null : holder.vanillaChunkHolder;
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- public ChunkHolder getVisibleChunkIfPresent(long pos) {
-- return (ChunkHolder) this.visibleChunkMap.get(pos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
-+ return holder == null ? null : holder.vanillaChunkHolder;
-+ // Paper end - rewrite chunk system
- }
-
- protected IntSupplier getChunkQueueLevel(long pos) {
-- return () -> {
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
-- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
-- };
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public String getChunkDebugData(ChunkPos chunkPos) {
-@@ -330,56 +313,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
-- if (margin == 0) {
-- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0);
--
-- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> {
-- return chunkresult.map(List::of);
-- });
-- } else {
-- int j = Mth.square(margin * 2 + 1);
-- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(j);
-- ChunkPos chunkcoordintpair = centerChunk.getPos();
--
-- for (int k = -margin; k <= margin; ++k) {
-- for (int l = -margin; l <= margin; ++l) {
-- int i1 = Math.max(Math.abs(l), Math.abs(k));
-- long j1 = ChunkPos.asLong(chunkcoordintpair.x + l, chunkcoordintpair.z + k);
-- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(j1);
--
-- if (playerchunk1 == null) {
-- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE;
-- }
--
-- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(i1);
--
-- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this));
-- }
-- }
--
-- return Util.sequence(list).thenApply((list1) -> {
-- List<ChunkAccess> list2 = new ArrayList(list1.size());
-- Iterator iterator = list1.iterator();
--
-- while (iterator.hasNext()) {
-- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next();
--
-- if (chunkresult == null) {
-- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
-- }
--
-- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
--
-- if (ichunkaccess == null) {
-- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
-- }
--
-- list2.add(ichunkaccess);
-- }
--
-- return ChunkResult.of(list2);
-- });
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
-@@ -409,104 +343,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) {
-- return this.getChunkRangeFuture(holder, 2, (i) -> {
-- return ChunkStatus.FULL;
-- }).thenApply((chunkresult) -> {
-- return chunkresult.map((list) -> {
-- return (LevelChunk) list.get(list.size() / 2);
-- });
-- });
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
- ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
-- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) {
-- return holder;
-- } else {
-- if (holder != null) {
-- holder.setTicketLevel(level);
-- }
--
-- if (holder != null) {
-- if (!ChunkLevel.isLoaded(level)) {
-- this.toDrop.add(pos);
-- } else {
-- this.toDrop.remove(pos);
-- }
-- }
--
-- if (ChunkLevel.isLoaded(level) && holder == null) {
-- holder = (ChunkHolder) this.pendingUnloads.remove(pos);
-- if (holder != null) {
-- holder.setTicketLevel(level);
-- } else {
-- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this);
-- // Paper start
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
-- // Paper end
-- }
--
-- this.updatingChunkMap.put(pos, holder);
-- this.modified = true;
-- }
--
-- return holder;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void onLevelChange(ChunkPos pos, IntSupplier levelGetter, int targetLevel, IntConsumer levelSetter) {
-- this.worldgenTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter);
-- this.lightTaskDispatcher.onLevelChange(pos, levelGetter, targetLevel, levelSetter);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public void close() throws IOException {
-- try {
-- this.worldgenTaskDispatcher.close();
-- this.lightTaskDispatcher.close();
-- this.poiManager.close();
-- } finally {
-- super.close();
-- }
-+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
-
- }
-
- protected void saveAllChunks(boolean flush) {
-- if (flush) {
-- 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 {
-- mutableboolean.setFalse();
-- list.stream().map((playerchunk) -> {
-- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
--
-- Objects.requireNonNull(playerchunk);
-- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving);
-- return playerchunk.getLatestChunk();
-- }).filter((ichunkaccess) -> {
-- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
-- }).filter(this::save).forEach((ichunkaccess) -> {
-- mutableboolean.setTrue();
-- });
-- } while (mutableboolean.isTrue());
--
-- this.poiManager.flushAll();
-- this.processUnloads(() -> {
-- return true;
-- });
-- this.flushWorker();
-- } else {
-- this.nextChunkSaveTime.clear();
-- long i = Util.getMillis();
-- Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
--
-- while (objectiterator.hasNext()) {
-- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
--
-- this.saveChunkIfNeeded(playerchunk, i);
-- }
-- }
-+ // Paper start - rewrite chunk system
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks(
-+ flush, false, false
-+ );
-+ // Paper end - rewrite chunk system
-
- }
-
-@@ -524,143 +384,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- public boolean hasWork() {
-- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void processUnloads(BooleanSupplier shouldKeepTicking) {
-- for (LongIterator longiterator = this.toDrop.iterator(); longiterator.hasNext(); longiterator.remove()) {
-- long i = longiterator.nextLong();
-- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(i);
--
-- if (playerchunk != null) {
-- this.updatingChunkMap.remove(i);
-- this.pendingUnloads.put(i, playerchunk);
-- this.modified = true;
-- this.scheduleUnload(i, playerchunk);
-- }
-- }
--
-- int j = Math.max(0, this.unloadQueue.size() - 2000);
--
-- Runnable runnable;
--
-- while ((j > 0 || shouldKeepTicking.getAsBoolean()) && (runnable = (Runnable) this.unloadQueue.poll()) != null) {
-- --j;
-- runnable.run();
-- }
--
-- this.saveChunksEagerly(shouldKeepTicking);
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system
- }
-
- private void saveChunksEagerly(BooleanSupplier shouldKeepTicking) {
-- long i = Util.getMillis();
-- int j = 0;
-- LongIterator longiterator = this.chunksToEagerlySave.iterator();
--
-- while (j < 20 && this.activeChunkWrites.get() < 128 && shouldKeepTicking.getAsBoolean() && longiterator.hasNext()) {
-- long k = longiterator.nextLong();
-- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(k);
-- ChunkAccess ichunkaccess = playerchunk != null ? playerchunk.getLatestChunk() : null;
--
-- if (ichunkaccess != null && ichunkaccess.isUnsaved()) {
-- if (this.saveChunkIfNeeded(playerchunk, i)) {
-- ++j;
-- longiterator.remove();
-- }
-- } else {
-- longiterator.remove();
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-
- }
-
- private void scheduleUnload(long pos, ChunkHolder chunk) {
-- CompletableFuture<?> completablefuture = chunk.getSaveSyncFuture();
-- Runnable runnable = () -> {
-- CompletableFuture<?> completablefuture1 = chunk.getSaveSyncFuture();
--
-- if (completablefuture1 != completablefuture) {
-- this.scheduleUnload(pos, chunk);
-- } else {
-- ChunkAccess ichunkaccess = chunk.getLatestChunk();
-- // Paper start
-- boolean removed;
-- if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) {
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
-- // Paper end
-- LevelChunk chunk1;
--
-- if (ichunkaccess instanceof LevelChunk) {
-- chunk1 = (LevelChunk) ichunkaccess;
-- chunk1.setLoaded(false);
-- }
--
-- this.save(ichunkaccess);
-- if (ichunkaccess instanceof LevelChunk) {
-- chunk1 = (LevelChunk) ichunkaccess;
-- this.level.unload(chunk1);
-- }
--
-- this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
-- this.lightEngine.tryScheduleUpdate();
-- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
-- this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong());
-- } else if (removed) { // Paper start
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
-- } // Paper end
--
-- }
-- };
-- Queue queue = this.unloadQueue;
--
-- Objects.requireNonNull(this.unloadQueue);
-- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> {
-- if (throwable != null) {
-- ChunkMap.LOGGER.error("Failed to save chunk {}", chunk.getPos(), throwable);
-- }
--
-- });
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- protected boolean promoteChunkMap() {
-- if (!this.modified) {
-- return false;
-- } else {
-- this.visibleChunkMap = this.updatingChunkMap.clone();
-- this.modified = false;
-- return true;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
-- CompletableFuture<Optional<SerializableChunkData>> completablefuture = this.readChunk(pos).thenApplyAsync((optional) -> {
-- return optional.map((nbttagcompound) -> {
-- SerializableChunkData serializablechunkdata = SerializableChunkData.parse(this.level, this.level.registryAccess(), nbttagcompound);
--
-- if (serializablechunkdata == null) {
-- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos);
-- }
--
-- return serializablechunkdata;
-- });
-- }, Util.backgroundExecutor().forName("parseChunk"));
-- CompletableFuture<?> completablefuture1 = this.poiManager.prefetch(pos);
--
-- return completablefuture.thenCombine(completablefuture1, (optional, object) -> {
-- return optional;
-- }).thenApplyAsync((optional) -> {
-- Profiler.get().incrementCounter("chunkLoad");
-- if (optional.isPresent()) {
-- ProtoChunk protochunk = ((SerializableChunkData) optional.get()).read(this.level, this.poiManager, this.storageInfo(), pos);
--
-- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType());
-- return protochunk;
-- } else {
-- return this.createEmptyChunk(pos);
-- }
-- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
-- return this.handleChunkLoadFailure(throwable, pos);
-- }, this.mainThreadExecutor);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private ChunkAccess handleChunkLoadFailure(Throwable throwable, ChunkPos chunkPos) {
-@@ -716,139 +462,43 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- @Override
- public GenerationChunkHolder acquireGeneration(long pos) {
-- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos);
--
-- playerchunk.increaseGenerationRefCount();
-- return playerchunk;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public void releaseGeneration(GenerationChunkHolder chunkHolder) {
-- chunkHolder.decreaseGenerationRefCount();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) {
-- ChunkPos chunkcoordintpair = chunkHolder.getPos();
--
-- if (step.targetStatus() == ChunkStatus.EMPTY) {
-- return this.scheduleChunkLoad(chunkcoordintpair);
-- } else {
-- try {
-- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z);
-- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent());
--
-- if (ichunkaccess == null) {
-- throw new IllegalStateException("Parent chunk missing");
-- } else {
-- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess);
--
-- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus());
-- return completablefuture;
-- }
-- } catch (Exception exception) {
-- exception.getStackTrace();
-- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
-- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
--
-- crashreportsystemdetails.setDetail("Status being generated", () -> {
-- return step.targetStatus().getName();
-- });
-- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
-- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
-- crashreportsystemdetails.setDetail("Generator", (Object) this.generator());
-- this.mainThreadExecutor.execute(() -> {
-- throw new ReportedException(crashreport);
-- });
-- throw new ReportedException(crashreport);
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) {
-- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos);
--
-- this.pendingGenerationTasks.add(chunkgenerationtask);
-- return chunkgenerationtask;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void runGenerationTask(ChunkGenerationTask loader) {
-- GenerationChunkHolder generationchunkholder = loader.getCenter();
-- ChunkTaskDispatcher chunktaskdispatcher = this.worldgenTaskDispatcher;
-- Runnable runnable = () -> {
-- CompletableFuture<?> completablefuture = loader.runUntilWait();
--
-- if (completablefuture != null) {
-- completablefuture.thenRun(() -> {
-- this.runGenerationTask(loader);
-- });
-- }
-- };
-- long i = generationchunkholder.getPos().toLong();
--
-- Objects.requireNonNull(generationchunkholder);
-- chunktaskdispatcher.submit(runnable, i, generationchunkholder::getQueueLevel);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public void runGenerationTasks() {
-- this.pendingGenerationTasks.forEach(this::runGenerationTask);
-- this.pendingGenerationTasks.clear();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
-- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> {
-- return ChunkStatus.FULL;
-- });
-- CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> {
-- return chunkresult.map((list) -> {
-- LevelChunk chunk = (LevelChunk) list.get(list.size() / 2);
--
-- chunk.postProcessGeneration(this.level);
-- this.level.startTickingChunk(chunk);
-- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture();
--
-- if (completablefuture2.isDone()) {
-- this.onChunkReadyToSend(holder, chunk);
-- } else {
-- completablefuture2.thenAcceptAsync((object) -> {
-- this.onChunkReadyToSend(holder, chunk);
-- }, this.mainThreadExecutor);
-- }
--
-- return chunk;
-- });
-- }, this.mainThreadExecutor);
--
-- completablefuture1.handle((chunkresult, throwable) -> {
-- this.tickingGenerated.getAndIncrement();
-- return null;
-- });
-- return completablefuture1;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) {
-- ChunkPos chunkcoordintpair = chunk.getPos();
-- Iterator iterator = this.playerMap.getAllPlayers().iterator();
--
-- while (iterator.hasNext()) {
-- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
-- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) {
-- ChunkMap.markChunkPendingToSend(entityplayer, chunk);
-- }
-- }
--
-- this.level.getChunkSource().onChunkReadyToSend(chunkHolder);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) {
-- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApply((chunkresult) -> {
-- return chunkresult.map((list) -> {
-- return (LevelChunk) list.get(list.size() / 2);
-- });
-- });
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public int getTickingGenerated() {
-@@ -856,144 +506,80 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- private boolean saveChunkIfNeeded(ChunkHolder chunkHolder, long currentTime) {
-- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) {
-- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk();
--
-- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) {
-- return false;
-- } else if (!ichunkaccess.isUnsaved()) {
-- return false;
-- } else {
-- long j = ichunkaccess.getPos().toLong();
-- long k = this.nextChunkSaveTime.getOrDefault(j, -1L);
--
-- if (currentTime < k) {
-- return false;
-- } else {
-- boolean flag = this.save(ichunkaccess);
--
-- chunkHolder.refreshAccessibility();
-- if (flag) {
-- this.nextChunkSaveTime.put(j, currentTime + 10000L);
-- }
--
-- return flag;
-- }
-- }
-- } else {
-- return false;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public boolean save(ChunkAccess chunk) {
-- this.poiManager.flush(chunk.getPos());
-- if (!chunk.tryMarkSaved()) {
-- return false;
-- } else {
-- ChunkPos chunkcoordintpair = chunk.getPos();
--
-- try {
-- ChunkStatus chunkstatus = chunk.getPersistedStatus();
--
-- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) {
-- if (this.isExistingChunkFull(chunkcoordintpair)) {
-- return false;
-- }
--
-- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
-- return false;
-- }
-- }
--
-- Profiler.get().incrementCounter("chunkSave");
-- this.activeChunkWrites.incrementAndGet();
-- SerializableChunkData serializablechunkdata = SerializableChunkData.copyOf(this.level, chunk);
--
-- Objects.requireNonNull(serializablechunkdata);
-- CompletableFuture<CompoundTag> completablefuture = CompletableFuture.supplyAsync(serializablechunkdata::write, Util.backgroundExecutor());
--
-- Objects.requireNonNull(completablefuture);
-- this.write(chunkcoordintpair, completablefuture::join).handle((ovoid, throwable) -> {
-- if (throwable != null) {
-- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair);
-- }
--
-- this.activeChunkWrites.decrementAndGet();
-- return null;
-- });
-- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
-- return true;
-- } catch (Exception exception) {
-- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair);
-- return false;
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private boolean isExistingChunkFull(ChunkPos pos) {
-- byte b0 = this.chunkTypeCache.get(pos.toLong());
--
-- if (b0 != 0) {
-- return b0 == 1;
-- } else {
-- CompoundTag nbttagcompound;
--
-- try {
-- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null);
-- if (nbttagcompound == null) {
-- this.markPositionReplaceable(pos);
-- return false;
-- }
-- } catch (Exception exception) {
-- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception);
-- this.markPositionReplaceable(pos);
-- return false;
-- }
--
-- ChunkType chunktype = SerializableChunkData.getChunkTypeFromTag(nbttagcompound);
--
-- return this.markPosition(pos, chunktype) == 1;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void setServerViewDistance(int watchDistance) { // Paper - public
-- int j = Mth.clamp(watchDistance, 2, 32);
--
-- if (j != this.serverViewDistance) {
-- this.serverViewDistance = j;
-- this.distanceManager.updatePlayerTickets(this.serverViewDistance);
-- Iterator iterator = this.playerMap.getAllPlayers().iterator();
--
-- while (iterator.hasNext()) {
-- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
-- this.updateChunkTracking(entityplayer);
-- }
-+ // Paper start - rewrite chunk system
-+ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
-+ if (clamped == this.serverViewDistance) {
-+ return;
- }
-
-+ this.serverViewDistance = clamped;
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1);
-+ // Paper end - rewrite chunk system
- }
-
- public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
-- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
-+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
- }
-
- private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
-- LevelChunk chunk = this.getChunkToSend(pos.toLong());
--
-- if (chunk != null) {
-- ChunkMap.markChunkPendingToSend(player, chunk);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-
- }
-
- private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
-- player.connection.chunkSender.markChunkPendingToSend(chunk);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private static void dropChunk(ServerPlayer player, ChunkPos pos) {
-- player.connection.chunkSender.dropChunk(player, pos);
-+ // Paper - rewrite chunk system
-+ }
-+
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) {
-+ final CompletableFuture<Optional<CompoundTag>> ret = new CompletableFuture<>();
-+
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadDataAsync(
-+ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA,
-+ (final CompoundTag data, final Throwable thr) -> {
-+ if (thr != null) {
-+ ret.completeExceptionally(thr);
-+ } else {
-+ ret.complete(Optional.ofNullable(data));
-+ }
-+ }, false
-+ );
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public CompletableFuture<Void> write(final ChunkPos pos, final Supplier<CompoundTag> tag) {
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave(
-+ this.level, pos.x, pos.z, tag.get(),
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.CHUNK_DATA
-+ );
-+ return null;
- }
-
-+ @Override
-+ public void flushWorker() {
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.flush(this.level);
-+ }
-+ // Paper end - rewrite chunk system
-+
- @Nullable
- public LevelChunk getChunkToSend(long pos) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-@@ -1059,7 +645,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- // CraftBukkit start
-- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
-+ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public
- return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
- // CraftBukkit end
- }
-@@ -1069,7 +655,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- while (longiterator.hasNext()) {
- long i = longiterator.nextLong();
-- ChunkHolder playerchunk = (ChunkHolder) this.visibleChunkMap.get(i);
-+ ChunkHolder playerchunk = (ChunkHolder) this.getVisibleChunkIfPresent(i); // Paper - rewrite chunk system
-
- if (playerchunk != null && this.anyPlayerCloseEnoughForSpawningInternal(playerchunk.getPos())) {
- callback.accept(playerchunk);
-@@ -1084,7 +670,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) {
-- return !this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong()) ? false : this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange);
-+ return this.anyPlayerCloseEnoughForSpawningInternal(chunkcoordintpair, reducedRange); // Paper - chunk tick iteration optimisation
- // Spigot end
- }
-
-@@ -1096,16 +682,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkcoordintpair, boolean reducedRange) {
- double blockRange; // Paper - use from event
- // Spigot end
-- Iterator iterator = this.playerMap.getAllPlayers().iterator();
--
-- ServerPlayer entityplayer;
-+ // Paper start - chunk tick iteration optimisation
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers(
-+ chunkcoordintpair, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
-+ );
-+ if (players == null) {
-+ return false;
-+ }
-
-- do {
-- if (!iterator.hasNext()) {
-- return false;
-- }
-+ final ServerPlayer[] raw = players.getRawDataUnchecked();
-+ final int len = players.size();
-
-- entityplayer = (ServerPlayer) iterator.next();
-+ Objects.checkFromIndexSize(0, len, raw.length);
-+ for (int i = 0; i < len; ++i) {
-+ final ServerPlayer entityplayer = raw[i];
- // Paper start - PlayerNaturallySpawnCreaturesEvent
- com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
- blockRange = 16384.0D;
-@@ -1115,33 +705,47 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
- }
- // Paper end - PlayerNaturallySpawnCreaturesEvent
-- } while (!this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)); // Spigot
-+ if (this.playerIsCloseEnoughForSpawning(entityplayer, chunkcoordintpair, blockRange)) {
-+ return true;
-+ }
-+ }
-
-- return true;
-+ return false;
-+ // Paper end - chunk tick iteration optimisation
- }
-
- public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) {
-- long i = pos.toLong();
-+ // Paper start - chunk tick iteration optimisation
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers().getPlayers(
-+ pos, ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE
-+ );
-+ if (players == null) {
-+ return new ArrayList<>();
-+ }
-
-- if (!this.distanceManager.hasPlayersNearby(i)) {
-- return List.of();
-- } else {
-- Builder<ServerPlayer> builder = ImmutableList.builder();
-- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-+ List<ServerPlayer> ret = null;
-
-- while (iterator.hasNext()) {
-- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+ final ServerPlayer[] raw = players.getRawDataUnchecked();
-+ final int len = players.size();
-
-- if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot
-- builder.add(entityplayer);
-+ Objects.checkFromIndexSize(0, len, raw.length);
-+ for (int i = 0; i < len; ++i) {
-+ final ServerPlayer player = raw[i];
-+ if (this.playerIsCloseEnoughForSpawning(player, pos, 16384.0D)) { // Spigot
-+ if (ret == null) {
-+ ret = new ArrayList<>(len - i);
-+ ret.add(player);
-+ } else {
-+ ret.add(player);
- }
- }
--
-- return builder.build();
- }
-+
-+ return ret == null ? new ArrayList<>() : ret;
-+ // Paper end - chunk tick iteration optimisation
- }
-
-- private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot
-+ public boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot // Paper - chunk tick iteration optimisation - public
- if (entityplayer.isSpectator()) {
- return false;
- } else {
-@@ -1164,19 +768,21 @@ 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.common.util.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
- } else {
- SectionPos sectionposition = player.getLastSectionPos();
-
- 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.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
- }
-
- }
-@@ -1188,17 +794,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- public void move(ServerPlayer player) {
-- ObjectIterator objectiterator = this.entityMap.values().iterator();
--
-- while (objectiterator.hasNext()) {
-- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
--
-- if (playerchunkmap_entitytracker.entity == player) {
-- playerchunkmap_entitytracker.updatePlayers(this.level.players());
-- } else {
-- playerchunkmap_entitytracker.updatePlayer(player);
-- }
-- }
-+ // Paper - optimise entity tracker
-
- SectionPos sectionposition = player.getLastSectionPos();
- SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
-@@ -1208,6 +804,7 @@ 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);
- }
-@@ -1224,70 +821,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.playerMap.unIgnorePlayer(player);
- }
-
-- this.updateChunkTracking(player);
-+ // Paper - rewrite chunk system
- }
-
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
- }
-
- private void updateChunkTracking(ServerPlayer player) {
-- ChunkPos chunkcoordintpair = player.chunkPosition();
-- int i = this.getPlayerViewDistance(player);
-- ChunkTrackingView chunktrackingview = player.getChunkTrackingView();
--
-- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) {
-- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) {
-- return;
-- }
-- }
--
-- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i));
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) {
-- if (player.level() == this.level) {
-- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView();
--
-- if (chunkFilter instanceof ChunkTrackingView.Positioned) {
-- label15:
-- {
-- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter;
--
-- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) {
-- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1;
--
-- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) {
-- break label15;
-- }
-- }
--
-- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z));
-- }
-- }
--
-- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> {
-- this.markChunkPendingToSend(player, chunkcoordintpair);
-- }, (chunkcoordintpair) -> {
-- ChunkMap.dropChunk(player, chunkcoordintpair);
-- });
-- player.setChunkTrackingView(chunkFilter);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
-- Set<ServerPlayer> set = this.playerMap.getAllPlayers();
-- Builder<ServerPlayer> builder = ImmutableList.builder();
-- Iterator iterator = set.iterator();
--
-- while (iterator.hasNext()) {
-- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
--
-- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) {
-- builder.add(entityplayer);
-- }
-+ // Paper start - rewrite chunk system
-+ final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
-+ if (holder == null) {
-+ return new ArrayList<>();
-+ } else {
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge);
- }
--
-- return builder.build();
-+ // Paper end - rewrite chunk system
- }
-
- public void addEntity(Entity entity) {
-@@ -1314,6 +871,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas());
-
- this.entityMap.put(entity.getId(), playerchunkmap_entitytracker);
-+ // Paper start - optimise entity tracker
-+ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) {
-+ throw new IllegalStateException("Entity is already tracked");
-+ }
-+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker);
-+ // Paper end - optimise entity tracker
- playerchunkmap_entitytracker.updatePlayers(this.level.players());
- if (entity instanceof ServerPlayer) {
- ServerPlayer entityplayer = (ServerPlayer) entity;
-@@ -1354,16 +917,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- playerchunkmap_entitytracker1.broadcastRemoved();
- }
-
-+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker
- }
-
-- protected void tick() {
-- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-+ // Paper start - optimise entity tracker
-+ private void newTrackerTick() {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();;
-
-- while (iterator.hasNext()) {
-- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
-+ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
-+ for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
-+ final Entity entity = trackerEntitiesRaw[i];
-+ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity();
-+ if (tracker == null) {
-+ continue;
-+ }
-+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers);
-+ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers()
-+ || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
-+ tracker.serverEntity.sendChanges();
-+ }
-+ }
-+ }
-+ // Paper end - optimise entity tracker
-
-- this.updateChunkTracking(entityplayer);
-+ protected void tick() {
-+ // Paper start - optimise entity tracker
-+ if (true) {
-+ this.newTrackerTick();
-+ return;
- }
-+ // Paper end - optimise entity tracker
-+ // Paper - rewrite chunk system
-
- List<ServerPlayer> list = Lists.newArrayList();
- List<ServerPlayer> list1 = this.level.players();
-@@ -1466,27 +1051,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
-- int j = radius + 1;
--
-- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> {
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong());
--
-- if (playerchunk != null) {
-- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z));
-- }
--
-- });
-+ // Paper - rewrite chunk system
- }
-
-- public class ChunkDistanceManager extends DistanceManager { // Paper - public
-+ public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
-
- protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
- super(workerExecutor, mainThreadExecutor);
- }
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final ChunkMap moonrise$getChunkMap() {
-+ return ChunkMap.this;
-+ }
-+ // Paper end - rewrite chunk system
-+
- @Override
- protected boolean isChunkToRemove(long pos) {
-- return ChunkMap.this.toDrop.contains(pos);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
-@@ -1502,7 +1085,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
- }
-
-- public class TrackedEntity {
-+ public class TrackedEntity implements ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity { // Paper - optimise entity tracker
-
- public final ServerEntity serverEntity;
- final Entity entity;
-@@ -1510,6 +1093,89 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- SectionPos lastSectionPos;
- public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
-
-+ // Paper start - optimise entity tracker
-+ private long lastChunkUpdate = -1L;
-+ private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk;
-+
-+ @Override
-+ public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) {
-+ if (chunk == null) {
-+ this.moonrise$clearPlayers();
-+ return;
-+ }
-+
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE);
-+
-+ if (players == null) {
-+ this.moonrise$clearPlayers();
-+ return;
-+ }
-+
-+ final long lastChunkUpdate = this.lastChunkUpdate;
-+ final long currChunkUpdate = chunk.getUpdateCount();
-+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk;
-+ this.lastChunkUpdate = currChunkUpdate;
-+ this.lastTrackedChunk = chunk;
-+
-+ final ServerPlayer[] playersRaw = players.getRawDataUnchecked();
-+
-+ for (int i = 0, len = players.size(); i < len; ++i) {
-+ final ServerPlayer player = playersRaw[i];
-+ this.updatePlayer(player);
-+ }
-+
-+ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) {
-+ // need to purge any players possible not in the chunk list
-+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
-+ final ServerPlayer player = conn.getPlayer();
-+ if (!players.contains(player)) {
-+ this.removePlayer(player);
-+ }
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$removeNonTickThreadPlayers() {
-+ boolean foundToRemove = false;
-+ for (final ServerPlayerConnection conn : this.seenBy) {
-+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(conn.getPlayer())) {
-+ foundToRemove = true;
-+ break;
-+ }
-+ }
-+
-+ if (!foundToRemove) {
-+ return;
-+ }
-+
-+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
-+ ServerPlayer player = conn.getPlayer();
-+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) {
-+ this.removePlayer(player);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$clearPlayers() {
-+ this.lastChunkUpdate = -1;
-+ this.lastTrackedChunk = null;
-+ if (this.seenBy.isEmpty()) {
-+ return;
-+ }
-+ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
-+ ServerPlayer player = conn.getPlayer();
-+ this.removePlayer(player);
-+ }
-+ }
-+
-+ @Override
-+ public final boolean moonrise$hasPlayers() {
-+ return !this.seenBy.isEmpty();
-+ }
-+ // Paper end - optimise entity tracker
-+
- public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) {
- this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
- this.entity = entity;
-@@ -1612,20 +1278,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- }
-
- private int getEffectiveRange() {
-- int i = this.range;
-- Iterator iterator = this.entity.getIndirectPassengers().iterator();
-+ // Paper start - optimise entity tracker
-+ final Entity entity = this.entity;
-+ int range = this.range;
-
-- while (iterator.hasNext()) {
-- Entity entity = (Entity) iterator.next();
-- int j = entity.getType().clientTrackingRange() * 16;
-- j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper
-+ if (entity.getPassengers() == ImmutableList.<Entity>of()) {
-+ return this.scaledRange(range);
-+ }
-
-- if (j > i) {
-- i = j;
-- }
-+ // note: we change to List
-+ final List<Entity> passengers = (List<Entity>)entity.getIndirectPassengers();
-+ for (int i = 0, len = passengers.size(); i < len; ++i) {
-+ final Entity passenger = passengers.get(i);
-+ // note: max should be branchless
-+ range = Math.max(range, ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(passenger, passenger.getType().clientTrackingRange() << 4));
- }
-
-- return this.scaledRange(i);
-+ return this.scaledRange(range);
-+ // Paper end - optimise entity tracker
- }
-
- public void updatePlayers(List<ServerPlayer> players) {
-diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java
-index f7c2c03749d6be25bf33afd61e1da120770b3432..746f61661e22d22f2acbbe54a5933e57fbca45b2 100644
---- a/net/minecraft/server/level/DistanceManager.java
-+++ b/net/minecraft/server/level/DistanceManager.java
-@@ -34,58 +34,57 @@ import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.chunk.LevelChunk;
- import org.slf4j.Logger;
-
--public abstract class DistanceManager {
-+public abstract class DistanceManager 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);
- private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
- final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
-- public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
-- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
-- 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 = new ReferenceOpenHashSet();
-- final ThrottlingChunkTaskDispatcher ticketDispatcher;
-- 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;
-+ // Paper - rewrite chunk system
-
- protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
- TaskScheduler<Runnable> taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor);
-
-- this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskscheduler, workerExecutor, 4);
-- this.mainThreadExecutor = mainThreadExecutor;
-+ // Paper - rewrite chunk system
- }
-
-- protected void purgeStaleTickets() {
-- ++this.ticketTickCounter;
-- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
--
-- while (objectiterator.hasNext()) {
-- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
-- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
-- boolean flag = false;
--
-- while (iterator.hasNext()) {
-- Ticket<?> ticket = (Ticket) iterator.next();
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager moonrise$getChunkHolderManager() {
-+ 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<>();
-
-- if (ticket.timedOut(this.ticketTickCounter)) {
-- iterator.remove();
-- flag = true;
-- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
-- }
-- }
-+ @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);
-+ }
-
-- if (flag) {
-- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
-- }
-+ @Override
-+ public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) {
-+ this.spawnChunkTracker.remove(player);
-+ }
-
-- if (((SortedArraySet) entry.getValue()).isEmpty()) {
-- objectiterator.remove();
-- }
-+ @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 void purgeStaleTickets() {
-+ this.moonrise$getChunkHolderManager().tick(); // Paper - rewrite chunk system
-
- }
-
-@@ -102,105 +101,15 @@ public abstract class DistanceManager {
- protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
-
- public boolean runAllUpdates(ChunkMap chunkLoadingManager) {
-- this.naturalSpawnChunkCounter.runAllUpdates();
-- this.tickingTicketsTracker.runAllUpdates();
-- this.playerTicketManager.runAllUpdates();
-- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
-- boolean flag = i != 0;
--
-- if (flag) {
-- ;
-- }
--
-- if (!this.chunksToUpdateFutures.isEmpty()) {
-- Iterator iterator = this.chunksToUpdateFutures.iterator();
--
-- ChunkHolder playerchunk;
--
-- // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
-- while (iterator.hasNext()) {
-- playerchunk = (ChunkHolder) iterator.next();
-- playerchunk.callEventIfUnloading(chunkLoadingManager);
-- }
--
-- iterator = this.chunksToUpdateFutures.iterator();
-- // CraftBukkit end
--
-- while (iterator.hasNext()) {
-- playerchunk = (ChunkHolder) iterator.next();
-- playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
-- }
--
-- iterator = this.chunksToUpdateFutures.iterator();
--
-- while (iterator.hasNext()) {
-- playerchunk = (ChunkHolder) iterator.next();
-- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor);
-- }
--
-- this.chunksToUpdateFutures.clear();
-- return true;
-- } else {
-- if (!this.ticketsToRelease.isEmpty()) {
-- LongIterator longiterator = this.ticketsToRelease.iterator();
--
-- while (longiterator.hasNext()) {
-- long j = longiterator.nextLong();
--
-- if (this.getTickets(j).stream().anyMatch((ticket) -> {
-- return ticket.getType() == TicketType.PLAYER;
-- })) {
-- ChunkHolder playerchunk1 = chunkLoadingManager.getUpdatingChunkIfPresent(j);
--
-- if (playerchunk1 == null) {
-- throw new IllegalStateException();
-- }
--
-- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk1.getEntityTickingChunkFuture();
--
-- completablefuture.thenAccept((chunkresult) -> {
-- this.mainThreadExecutor.execute(() -> {
-- this.ticketDispatcher.release(j, () -> {
-- }, false);
-- });
-- });
-- }
-- }
--
-- this.ticketsToRelease.clear();
-- }
--
-- return flag;
-- }
-+ return this.moonrise$getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system
- }
-
- boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
-- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
-- int j = DistanceManager.getTicketLevelAt(arraysetsorted);
-- Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
--
-- ticket1.setCreatedTick(this.ticketTickCounter);
-- if (ticket.getTicketLevel() < j) {
-- this.ticketTracker.update(i, ticket.getTicketLevel(), true);
-- }
--
-- return ticket == ticket1; // CraftBukkit
-+ return this.moonrise$getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
- }
-
- boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
-- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
--
-- boolean removed = false; // CraftBukkit
-- if (arraysetsorted.remove(ticket)) {
-- removed = true; // CraftBukkit
-- }
--
-- if (arraysetsorted.isEmpty()) {
-- this.tickets.remove(i);
-- }
--
-- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
-- return removed; // CraftBukkit
-+ return this.moonrise$getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
- }
-
- public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
-@@ -219,13 +128,7 @@ public abstract class DistanceManager {
- }
-
- public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
-- // CraftBukkit end
-- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
-- long j = chunkcoordintpair.toLong();
--
-- boolean added = this.addTicket(j, ticket); // CraftBukkit
-- this.tickingTicketsTracker.addTicket(j, ticket);
-- return added; // CraftBukkit
-+ return this.moonrise$getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
- }
-
- public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
-@@ -234,32 +137,21 @@ public abstract class DistanceManager {
- }
-
- public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
-- // CraftBukkit end
-- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
-- long j = chunkcoordintpair.toLong();
--
-- boolean removed = this.removeTicket(j, ticket); // CraftBukkit
-- this.tickingTicketsTracker.removeTicket(j, ticket);
-- return removed; // CraftBukkit
-+ return this.moonrise$getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
- }
-
- private SortedArraySet<Ticket<?>> getTickets(long position) {
-- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> {
-- return SortedArraySet.create(4);
-- });
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- protected void updateChunkForced(ChunkPos pos, boolean forced) {
-- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos);
-- long i = pos.toLong();
--
-+ // Paper start - rewrite chunk system
- if (forced) {
-- this.addTicket(i, ticket);
-- this.tickingTicketsTracker.addTicket(i, ticket);
-+ this.moonrise$getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
- } else {
-- this.removeTicket(i, ticket);
-- this.tickingTicketsTracker.removeTicket(i, ticket);
-+ this.moonrise$getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
- }
-+ // Paper end - rewrite chunk system
-
- }
-
-@@ -270,9 +162,8 @@ public abstract class DistanceManager {
- ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
- return new ObjectOpenHashSet();
- })).add(player);
-- this.naturalSpawnChunkCounter.update(i, 0, true);
-- this.playerTicketManager.update(i, 0, true);
-- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
-+ // Paper - chunk tick iteration optimisation
-+ // Paper - rewrite chunk system
- }
-
- public void removePlayer(SectionPos pos, ServerPlayer player) {
-@@ -284,160 +175,93 @@ 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.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
- }
-
- }
-
- private int getPlayerTicketLevel() {
-- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public boolean inEntityTickingRange(long chunkPos) {
-- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos));
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkHolderManager().getChunkHolder(chunkPos);
-+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- public boolean inBlockTickingRange(long chunkPos) {
-- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos));
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkHolderManager().getChunkHolder(chunkPos);
-+ return chunkHolder != null && chunkHolder.isTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- protected String getTicketDebugString(long pos) {
-- SortedArraySet<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos);
--
-- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket";
-+ return this.moonrise$getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system
- }
-
- protected void updatePlayerTickets(int viewDistance) {
-- this.playerTicketManager.updateViewDistance(viewDistance);
-+ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system
- }
-
- public void updateSimulationDistance(int simulationDistance) {
-- if (simulationDistance != this.simulationDistance) {
-- this.simulationDistance = simulationDistance;
-- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel());
-- }
-+ // Paper start - rewrite chunk system
-+ // note: vanilla does not clamp to 0, but we do simply because we need a min of 0
-+ final int clamped = net.minecraft.util.Mth.clamp(simulationDistance, 0, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
-
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(clamped);
-+ // Paper end - rewrite chunk system
- }
-
- 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 LongIterator getSpawnCandidateChunks() {
-- this.naturalSpawnChunkCounter.runAllUpdates();
-- return this.naturalSpawnChunkCounter.chunks.keySet().iterator();
-+ return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation
- }
-
- public String getDebugStatus() {
-- return this.ticketDispatcher.getDebugStatus();
-+ return "No DistanceManager stats available"; // Paper - rewrite chunk system
- }
-
- private void dumpTickets(String path) {
-- try {
-- FileOutputStream fileoutputstream = new FileOutputStream(new File(path));
--
-- try {
-- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator();
--
-- while (objectiterator.hasNext()) {
-- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
-- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey());
-- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator();
--
-- while (iterator.hasNext()) {
-- Ticket<?> ticket = (Ticket) iterator.next();
--
-- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8));
-- }
-- }
-- } catch (Throwable throwable) {
-- try {
-- fileoutputstream.close();
-- } catch (Throwable throwable1) {
-- throwable.addSuppressed(throwable1);
-- }
--
-- throw throwable;
-- }
--
-- fileoutputstream.close();
-- } catch (IOException ioexception) {
-- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
-
- }
-
- @VisibleForTesting
- TickingTracker tickingTracker() {
-- return this.tickingTicketsTracker;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public LongSet getTickingChunks() {
-- return this.tickingTicketsTracker.getTickingChunks();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void removeTicketsOnClosing() {
-- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
-- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
--
-- while (objectiterator.hasNext()) {
-- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
-- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
-- boolean flag = false;
--
-- while (iterator.hasNext()) {
-- Ticket<?> ticket = (Ticket) iterator.next();
--
-- if (!immutableset.contains(ticket.getType())) {
-- iterator.remove();
-- flag = true;
-- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
-- }
-- }
--
-- if (flag) {
-- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
-- }
--
-- if (((SortedArraySet) entry.getValue()).isEmpty()) {
-- objectiterator.remove();
-- }
-- }
-+ // Paper - rewrite chunk system
-
- }
-
- public boolean hasTickets() {
-- return !this.tickets.isEmpty();
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- // CraftBukkit start
- public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
-- Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
--
-- for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-- Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
-- SortedArraySet<Ticket<?>> tickets = entry.getValue();
-- if (tickets.remove(target)) {
-- // copied from removeTicket
-- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
--
-- // can't use entry after it's removed
-- if (tickets.isEmpty()) {
-- iterator.remove();
-- }
-- }
-- }
-+ this.moonrise$getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system
- }
- // CraftBukkit end
-
-+ /* // Paper - rewrite chunk system
- private class ChunkTicketTracker extends ChunkTracker {
-
- private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
-@@ -483,7 +307,7 @@ public abstract class DistanceManager {
- public int runDistanceUpdates(int distance) {
- return this.runUpdates(distance);
- }
-- }
-+ }*/ // Paper - rewrite chunk system
-
- private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
-
-@@ -563,6 +387,7 @@ public abstract class DistanceManager {
- }
- }
-
-+ /* // Paper - rewrite chunk system
- private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
-
- private int viewDistance = 0;
-@@ -657,5 +482,5 @@ public abstract class DistanceManager {
- private boolean haveTicketFor(int distance) {
- return distance <= this.viewDistance;
- }
-- }
-+ }*/ // Paper - rewrite chunk system
- }
-diff --git a/net/minecraft/server/level/GenerationChunkHolder.java b/net/minecraft/server/level/GenerationChunkHolder.java
-index 65206fdfa5b94eaca139e433b4865c16b16641f3..bf4463bcb5dc439ac5a3fa08dd60845a5fd7489a 100644
---- a/net/minecraft/server/level/GenerationChunkHolder.java
-+++ b/net/minecraft/server/level/GenerationChunkHolder.java
-@@ -27,13 +27,7 @@ public abstract class GenerationChunkHolder {
- public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk");
- public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK);
- protected final ChunkPos pos;
-- @Nullable
-- private volatile ChunkStatus highestAllowedStatus;
-- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>();
-- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size());
-- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>();
-- private final AtomicInteger generationRefCount = new AtomicInteger();
-- private volatile CompletableFuture<Void> generationSaveSyncFuture = CompletableFuture.completedFuture(null);
-+ // Paper - rewrite chunk system
-
- public GenerationChunkHolder(ChunkPos pos) {
- this.pos = pos;
-@@ -43,243 +37,96 @@ public abstract class GenerationChunkHolder {
- }
-
- public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) {
-- if (this.isStatusDisallowed(requestedStatus)) {
-- return UNLOADED_CHUNK_FUTURE;
-- } else {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus);
-- if (completableFuture.isDone()) {
-- return completableFuture;
-- } else {
-- ChunkGenerationTask chunkGenerationTask = this.task.get();
-- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) {
-- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus);
-- }
--
-- return completableFuture;
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) {
-- if (this.isStatusDisallowed(step.targetStatus())) {
-- return UNLOADED_CHUNK_FUTURE;
-- } else {
-- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> {
-- if (throwable != null) {
-- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading");
-- MinecraftServer.setFatalException(new ReportedException(crashReport));
-- } else {
-- this.completeFuture(step.targetStatus(), chunk);
-- }
--
-- return ChunkResult.of(chunk);
-- }) : this.getOrCreateFuture(step.targetStatus());
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) {
-- ChunkStatus chunkStatus = this.highestAllowedStatus;
-- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel());
-- this.highestAllowedStatus = chunkStatus2;
-- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus));
-- if (bl) {
-- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus);
-- if (this.task.get() != null) {
-- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2));
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void replaceProtoChunk(ImposterProtoChunk chunk) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk));
--
-- for (int i = 0; i < this.futures.length() - 1; i++) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i);
-- Objects.requireNonNull(completableFuture2);
-- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null);
-- if (!(chunkAccess instanceof ProtoChunk)) {
-- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess);
-- }
--
-- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) {
-- throw new IllegalStateException("Future changed by other thread while trying to replace it");
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- void removeTask(ChunkGenerationTask loader) {
-- this.task.compareAndSet(loader, null);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) {
-- ChunkGenerationTask chunkGenerationTask;
-- if (requestedStatus != null) {
-- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos());
-- } else {
-- chunkGenerationTask = null;
-- }
--
-- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask);
-- if (chunkGenerationTask3 != null) {
-- chunkGenerationTask3.markForCancellation();
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) {
-- if (this.isStatusDisallowed(status)) {
-- return UNLOADED_CHUNK_FUTURE;
-- } else {
-- int i = status.getIndex();
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
--
-- while (completableFuture == null) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>();
-- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2);
-- if (completableFuture == null) {
-- if (this.isStatusDisallowed(status)) {
-- this.failAndClearPendingFuture(i, completableFuture2);
-- return UNLOADED_CHUNK_FUTURE;
-- }
--
-- return completableFuture2;
-- }
-- }
--
-- return completableFuture;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) {
-- int i = from == null ? 0 : from.getIndex() + 1;
-- int j = to.getIndex();
--
-- for (int k = i; k <= j; k++) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k);
-- if (completableFuture != null) {
-- this.failAndClearPendingFuture(k, completableFuture);
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) {
-- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) {
-- throw new IllegalStateException("Nothing else should replace the future here");
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void completeFuture(ChunkStatus status, ChunkAccess chunk) {
-- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk);
-- int i = status.getIndex();
--
-- while (true) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
-- if (completableFuture == null) {
-- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) {
-- return;
-- }
-- } else {
-- if (completableFuture.complete(chunkResult)) {
-- return;
-- }
--
-- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) {
-- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already");
-- }
--
-- Thread.yield();
-- }
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
- private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) {
-- if (checkUpperBound == null) {
-- return null;
-- } else {
-- ChunkStatus chunkStatus = checkUpperBound;
--
-- for (ChunkStatus chunkStatus2 = this.startedWork.get();
-- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2);
-- chunkStatus = chunkStatus.getParent()
-- ) {
-- if (this.futures.get(chunkStatus.getIndex()) != null) {
-- return chunkStatus;
-- }
--
-- if (chunkStatus == ChunkStatus.EMPTY) {
-- break;
-- }
-- }
--
-- return null;
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private boolean acquireStatusBump(ChunkStatus nextStatus) {
-- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent();
-- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus);
-- if (chunkStatus2 == chunkStatus) {
-- return true;
-- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) {
-- return false;
-- } else {
-- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private boolean isStatusDisallowed(ChunkStatus status) {
-- ChunkStatus chunkStatus = this.highestAllowedStatus;
-- return chunkStatus == null || status.isAfter(chunkStatus);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- protected abstract void addSaveDependency(CompletableFuture<?> savingFuture);
-
- public void increaseGenerationRefCount() {
-- if (this.generationRefCount.getAndIncrement() == 0) {
-- this.generationSaveSyncFuture = new CompletableFuture<>();
-- this.addSaveDependency(this.generationSaveSyncFuture);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void decreaseGenerationRefCount() {
-- CompletableFuture<Void> completableFuture = this.generationSaveSyncFuture;
-- int i = this.generationRefCount.decrementAndGet();
-- if (i == 0) {
-- completableFuture.complete(null);
-- }
--
-- if (i < 0) {
-- throw new IllegalStateException("More releases than claims. Count: " + i);
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
- public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex());
-- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
-+ // Paper start - rewrite chunk system
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus);
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
-- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
-+ // Paper start - rewrite chunk system
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus);
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- public ChunkAccess getLatestChunk() {
-- ChunkStatus chunkStatus = this.startedWork.get();
-- if (chunkStatus == null) {
-- return null;
-- } else {
-- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
-- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
-- }
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
-+ return lastCompletion == null ? null : lastCompletion.chunk();
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- public ChunkStatus getPersistedStatus() {
-- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex());
-- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
-- return chunkAccess == null ? null : chunkAccess.getPersistedStatus();
-+ // Paper start - rewrite chunk system
-+ final ChunkAccess chunk = this.getLatestChunk();
-+ return chunk == null ? null : chunk.getPersistedStatus();
-+ // Paper end - rewrite chunk system
- }
-
- public ChunkPos getPos() {
-@@ -287,7 +134,7 @@ public abstract class GenerationChunkHolder {
- }
-
- public FullChunkStatus getFullStatus() {
-- return ChunkLevel.fullStatus(this.getTicketLevel());
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system
- }
-
- public abstract int getTicketLevel();
-@@ -296,26 +143,15 @@ public abstract class GenerationChunkHolder {
-
- @VisibleForDebug
- public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
-- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>();
--
-- for (int i = 0; i < CHUNK_STATUSES.size(); i++) {
-- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i)));
-- }
--
-- return list;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Nullable
- @VisibleForDebug
- public ChunkStatus getLatestStatus() {
-- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) {
-- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i);
-- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
-- if (chunkAccess != null) {
-- return chunkStatus;
-- }
-- }
--
-- return null;
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
-+ return lastCompletion == null ? null : lastCompletion.genStatus();
-+ // Paper end - rewrite chunk system
- }
- }
-diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
-index 6a2af3cd3aebe525a5ff41a801929547d59b8fec..d7382fc1498a33db909c343d8d07c5aa7130c20f 100644
---- a/net/minecraft/server/level/ServerChunkCache.java
-+++ b/net/minecraft/server/level/ServerChunkCache.java
-@@ -52,7 +52,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
- import net.minecraft.world.level.storage.LevelStorageSource;
- import org.slf4j.Logger;
-
--public class ServerChunkCache extends ChunkSource {
-+public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system
-
- private static final Logger LOGGER = LogUtils.getLogger();
- private final DistanceManager distanceManager;
-@@ -81,6 +81,107 @@ public class ServerChunkCache extends ChunkSource {
- }
- long chunkFutureAwaitCounter;
- // Paper end
-+ // Paper start - rewrite chunk system
-+
-+ @Override
-+ public final void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk) {
-+ final long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ if (chunk == null) {
-+ this.fullChunks.remove(key);
-+ } else {
-+ this.fullChunks.put(key, chunk);
-+ }
-+ }
-+
-+ @Override
-+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
-+ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
-+ final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>();
-+ chunkTaskScheduler.scheduleChunkLoad(
-+ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING,
-+ completable::complete
-+ );
-+
-+ if (!completable.isDone() && chunkTaskScheduler.hasShutdown()) {
-+ throw new IllegalStateException(
-+ "Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.level) + "' at "
-+ + "(" + chunkX + "," + chunkZ + ") status: " + toStatus
-+ );
-+ }
-+
-+ 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();
-+ }
-+
-+ final ChunkAccess ret = completable.join();
-+ if (ret == null) {
-+ throw new IllegalStateException("Chunk not loaded when requested");
-+ }
-+
-+ return ret;
-+ }
-+
-+ private ChunkAccess getChunkFallback(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-+ final boolean load) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ final ChunkAccess ifPresent = currentChunk == null ? null : currentChunk.getChunkIfPresent(toStatus);
-+
-+ if (ifPresent != null && (toStatus != ChunkStatus.FULL || currentChunk.isFullChunkReady())) {
-+ return ifPresent;
-+ }
-+
-+ final ca.spottedleaf.moonrise.common.PlatformHooks platformHooks = ca.spottedleaf.moonrise.common.PlatformHooks.get();
-+
-+ if (platformHooks.hasCurrentlyLoadingChunk() && currentChunk != null) {
-+ final ChunkAccess loading = platformHooks.getCurrentlyLoadingChunk(currentChunk.vanillaChunkHolder);
-+ if (loading != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) {
-+ return loading;
-+ }
-+ }
-+
-+ return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null;
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - chunk tick iteration optimisations
-+ private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L);
-+ private boolean isChunkNearPlayer(final ChunkMap chunkMap, final ChunkPos chunkPos, final LevelChunk levelChunk) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkAndHolder().holder())
-+ .moonrise$getRealChunkHolder().holderData;
-+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = chunkData.nearbyPlayers;
-+ if (nearbyPlayers == null) {
-+ return false;
-+ }
-+
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = nearbyPlayers.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.SPAWN_RANGE);
-+
-+ if (players == null) {
-+ return false;
-+ }
-+
-+ final ServerPlayer[] raw = players.getRawDataUnchecked();
-+ final int len = players.size();
-+
-+ Objects.checkFromIndexSize(0, len, raw.length);
-+ for (int i = 0; i < len; ++i) {
-+ if (chunkMap.playerIsCloseEnoughForSpawning(raw[i], chunkPos, 16384.0D)) { // Spigot (reducedRange = false)
-+ return true;
-+ }
-+ }
-+
-+ return false;
-+ }
-+ // Paper end - chunk tick iteration optimisations
-+
-
- public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
- this.level = world;
-@@ -112,13 +213,7 @@ public class ServerChunkCache extends ChunkSource {
- }
- // CraftBukkit end
- // Paper start
-- public void addLoadedChunk(LevelChunk chunk) {
-- this.fullChunks.put(chunk.coordinateKey, chunk);
-- }
--
-- public void removeLoadedChunk(LevelChunk chunk) {
-- this.fullChunks.remove(chunk.coordinateKey);
-- }
-+ // Paper - rewrite chunk system
-
- @Nullable
- public ChunkAccess getChunkAtImmediately(int x, int z) {
-@@ -189,59 +284,42 @@ public class ServerChunkCache extends ChunkSource {
- @Nullable
- @Override
- public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
-- if (Thread.currentThread() != this.mainThread) {
-- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
-- return this.getChunk(x, z, leastStatus, create);
-- }, this.mainThreadProcessor).join();
-- } else {
-- // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
-- LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
-- if (ifLoaded != null) {
-- return ifLoaded;
-- }
-- // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
-- ProfilerFiller gameprofilerfiller = Profiler.get();
-+ // Paper start - rewrite chunk system
-+ if (leastStatus == ChunkStatus.FULL) {
-+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
-
-- gameprofilerfiller.incrementCounter("getChunk");
-- long k = ChunkPos.asLong(x, z);
--
-- for (int l = 0; l < 4; ++l) {
-- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
-- ChunkAccess ichunkaccess = this.lastChunk[l];
--
-- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
-- return ichunkaccess;
-- }
-- }
-+ if (ret != null) {
-+ return ret;
- }
-
-- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
-- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
-- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
--
-- Objects.requireNonNull(completablefuture);
-- chunkproviderserver_b.managedBlock(completablefuture::isDone);
-- // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
-- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
-- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
--
-- if (ichunkaccess1 == null && create) {
-- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
-- } else {
-- this.storeInCache(k, ichunkaccess1, leastStatus);
-- return ichunkaccess1;
-- }
-+ return create ? this.getChunkFallback(x, z, leastStatus, create) : null;
- }
-+
-+ return this.getChunkFallback(x, z, leastStatus, create);
-+ // Paper end - rewrite chunk system
- }
-
- @Nullable
- @Override
- public LevelChunk getChunkNow(int chunkX, int chunkZ) {
-- if (Thread.currentThread() != this.mainThread) {
-- return null;
-- } else {
-- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
-+ // Paper start - rewrite chunk system
-+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (!ca.spottedleaf.moonrise.common.PlatformHooks.get().hasCurrentlyLoadingChunk()) {
-+ return ret;
-+ }
-+
-+ if (ret != null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) {
-+ return ret;
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler()
-+ .chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+ if (holder == null) {
-+ return ret;
- }
-+
-+ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getCurrentlyLoadingChunk(holder.vanillaChunkHolder);
-+ // Paper end - rewrite chunk system
- }
-
- private void clearCache() {
-@@ -272,56 +350,59 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
-- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
-- long k = chunkcoordintpair.toLong();
-- int l = ChunkLevel.byStatus(leastStatus);
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
--
-- // CraftBukkit start - don't add new ticket for currently unloading chunk
-- boolean currentlyUnloading = false;
-- if (playerchunk != null) {
-- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
-- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
-- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
-- }
-- if (create && !currentlyUnloading) {
-- // CraftBukkit end
-- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
-- if (this.chunkAbsent(playerchunk, l)) {
-- ProfilerFiller gameprofilerfiller = Profiler.get();
--
-- gameprofilerfiller.push("chunkLoad");
-- this.runDistanceManagerUpdates();
-- playerchunk = this.getVisibleChunkIfPresent(k);
-- gameprofilerfiller.pop();
-- if (this.chunkAbsent(playerchunk, l)) {
-- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
-- }
-- }
-+ // Paper start - rewrite chunk system
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
-+
-+ final int minLevel = ChunkLevel.byStatus(leastStatus);
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+
-+ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
-+
-+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) {
-+ return ChunkHolder.UNLOADED_CHUNK_FUTURE;
- }
-
-- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap);
-- }
-+ final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus);
-+ if (needsFullScheduling || ifPresent == null) {
-+ // schedule
-+ final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
-+ final Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> {
-+ if (chunk == null) {
-+ ret.complete(ChunkHolder.UNLOADED_CHUNK);
-+ } else {
-+ ret.complete(ChunkResult.of(chunk));
-+ }
-+ };
-
-- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
-- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
-+ chunkX, chunkZ, leastStatus, true,
-+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
-+ complete
-+ );
-+
-+ return ret;
-+ } else {
-+ // can return now
-+ return CompletableFuture.completedFuture(ChunkResult.of(ifPresent));
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- @Override
- public boolean hasChunk(int x, int z) {
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong());
-- int k = ChunkLevel.byStatus(ChunkStatus.FULL);
--
-- return !this.chunkAbsent(playerchunk, k);
-+ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system
- }
-
- @Nullable
- @Override
- public LightChunk getChunkForLighting(int chunkX, int chunkZ) {
-- long k = ChunkPos.asLong(chunkX, chunkZ);
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
--
-- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+ if (newChunkHolder == null) {
-+ return null;
-+ }
-+ return newChunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
-+ // Paper end - rewrite chunk system
- }
-
- @Override
-@@ -334,30 +415,18 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- public boolean runDistanceManagerUpdates() { // Paper - public
-- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
-- boolean flag1 = this.chunkMap.promoteChunkMap();
--
-- this.chunkMap.runGenerationTasks();
-- if (!flag && !flag1) {
-- return false;
-- } else {
-- this.clearCache();
-- return true;
-- }
-+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
- }
-
- public boolean isPositionTicking(long pos) {
-- if (!this.level.shouldTickBlocksAt(pos)) {
-- return false;
-- } else {
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
-- return playerchunk == null ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess();
-- }
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
-+ return newChunkHolder != null && newChunkHolder.isTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- public void save(boolean flush) {
-- this.runDistanceManagerUpdates();
-+ // Paper - rewrite chunk system
- this.chunkMap.saveAllChunks(flush);
- }
-
-@@ -368,17 +437,15 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- public void close(boolean save) throws IOException {
-- if (save) {
-- this.save(true);
-- }
- // CraftBukkit end
-+ // Paper - rewrite chunk system
- this.dataStorage.close();
-- this.lightEngine.close();
-- this.chunkMap.close();
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(save, true); // Paper - rewrite chunk system
- }
-
- // CraftBukkit start - modelled on below
- public void purgeUnload() {
-+ if (true) return; // Paper - rewrite chunk system
- ProfilerFiller gameprofilerfiller = Profiler.get();
-
- gameprofilerfiller.push("purge");
-@@ -403,6 +470,7 @@ public class ServerChunkCache extends ChunkSource {
- this.runDistanceManagerUpdates();
- gameprofilerfiller.popPush("chunks");
- if (tickChunks) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system
- this.tickChunks();
- this.chunkMap.tick();
- }
-@@ -429,7 +497,10 @@ public class ServerChunkCache extends ChunkSource {
- gameprofilerfiller.push("filteringTickingChunks");
- this.collectTickingChunks(list);
- gameprofilerfiller.popPush("shuffleChunks");
-- Util.shuffle(list, this.level.random);
-+ // Paper start - chunk tick iteration optimisation
-+ this.shuffleRandom.setSeed(this.level.random.nextLong());
-+ Util.shuffle(list, this.shuffleRandom);
-+ // Paper end - chunk tick iteration optimisation
- this.tickChunks(gameprofilerfiller, j, list);
- gameprofilerfiller.pop();
- } finally {
-@@ -448,7 +519,7 @@ public class ServerChunkCache extends ChunkSource {
-
- while (iterator.hasNext()) {
- ChunkHolder playerchunk = (ChunkHolder) iterator.next();
-- LevelChunk chunk = playerchunk.getTickingChunk();
-+ LevelChunk chunk = playerchunk.getChunkToSend(); // Paper - rewrite chunk system
-
- if (chunk != null) {
- playerchunk.broadcastChanges(chunk);
-@@ -460,14 +531,26 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- private void collectTickingChunks(List<LevelChunk> chunks) {
-- this.chunkMap.forEachSpawnCandidateChunk((playerchunk) -> {
-- LevelChunk chunk = playerchunk.getTickingChunk();
-+ // Paper start - chunk tick iteration optimisation
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks =
-+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks();
-+
-+ final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked();
-+ final int size = tickingChunks.size();
-
-- if (chunk != null && this.level.isNaturalSpawningAllowed(playerchunk.getPos())) {
-- chunks.add(chunk);
-+ final ChunkMap chunkMap = this.chunkMap;
-+
-+ for (int i = 0; i < size; ++i) {
-+ final ServerChunkCache.ChunkAndHolder chunkAndHolder = raw[i];
-+ final LevelChunk levelChunk = chunkAndHolder.chunk();
-+
-+ if (!this.isChunkNearPlayer(chunkMap, levelChunk.getPos(), levelChunk)) {
-+ continue;
- }
-
-- });
-+ chunks.add(levelChunk);
-+ }
-+ // Paper end - chunk tick iteration optimisation
- }
-
- private void tickChunks(ProfilerFiller profiler, long timeDelta, List<LevelChunk> chunks) {
-@@ -508,7 +591,7 @@ public class ServerChunkCache extends ChunkSource {
- NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, list1);
- }
-
-- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
-+ if (true) { // Paper - rewrite chunk system
- this.level.tickChunk(chunk, k);
- }
- }
-@@ -521,11 +604,13 @@ public class ServerChunkCache extends ChunkSource {
- }
-
- private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
-- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
--
-- if (playerchunk != null) {
-- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer);
-+ // Paper start - rewrite chunk system
-+ // note: bypass currentlyLoaded from getChunkNow
-+ final LevelChunk fullChunk = this.fullChunks.get(pos);
-+ if (fullChunk != null) {
-+ chunkConsumer.accept(fullChunk);
- }
-+ // Paper end - rewrite chunk system
-
- }
-
-@@ -619,6 +704,12 @@ public class ServerChunkCache extends ChunkSource {
- this.chunkMap.setServerViewDistance(watchDistance);
- }
-
-+ // Paper start - rewrite chunk system
-+ public void setSendViewDistance(int viewDistance) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setSendDistance(viewDistance);
-+ }
-+ // Paper end - rewrite chunk system
-+
- public void setSimulationDistance(int simulationDistance) {
- this.distanceManager.updateSimulationDistance(simulationDistance);
- }
-@@ -710,21 +801,19 @@ public class ServerChunkCache extends ChunkSource {
- @Override
- // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
- public boolean pollTask() {
-- try {
-- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
-+ // Paper start - rewrite chunk system
-+ final ServerChunkCache serverChunkCache = ServerChunkCache.this;
-+ if (serverChunkCache.runDistanceManagerUpdates()) {
- return true;
- } else {
-- ServerChunkCache.this.lightEngine.tryScheduleUpdate();
-- return super.pollTask();
-+ return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask();
- }
-- } finally {
-- ServerChunkCache.this.chunkMap.callbackExecutor.run();
-- }
-+ // Paper end - rewrite chunk system
- // 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/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java
-index d5bc702f2676b1b7a32c8f3a4a349fc2710ee825..301e8d6599d200cb0f1328f0e386af2f9a619939 100644
---- a/net/minecraft/server/level/ServerEntity.java
-+++ b/net/minecraft/server/level/ServerEntity.java
-@@ -101,6 +101,11 @@ public class ServerEntity {
- }
-
- public void sendChanges() {
-+ // Paper start - optimise collisions
-+ if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) {
-+ this.teleportDelay = 9999;
-+ }
-+ // Paper end - optimise collisions
- List<Entity> list = this.entity.getPassengers();
-
- if (!list.equals(this.lastPassengers)) {
-diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index 6c71ef3c7430623900a7021f853d2bb514273e4d..cf692267c6376ed8484478dc90f4f905d8325618 100644
---- a/net/minecraft/server/level/ServerLevel.java
-+++ b/net/minecraft/server/level/ServerLevel.java
-@@ -186,7 +186,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
- import org.bukkit.event.world.TimeSkipEvent;
- // CraftBukkit end
-
--public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel {
-+public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration
-
- public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
- public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
-@@ -202,7 +202,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- public final PrimaryLevelData serverLevelData; // CraftBukkit - type
- private int lastSpawnChunkRadius;
- final EntityTickList entityTickList = new EntityTickList();
-- public final PersistentEntitySectionManager<Entity> entityManager;
-+ // Paper - rewrite chunk system
- private final GameEventDispatcher gameEventDispatcher;
- public boolean noSave;
- private final SleepStatus sleepStatus;
-@@ -273,12 +273,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
- java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-- if (Thread.currentThread() != this.thread) {
-- this.getChunkSource().mainThreadProcessor.execute(() -> {
-- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
-- });
-- return;
-- }
-+ // Paper - rewrite chunk system
- int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
- int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
-
-@@ -297,32 +292,159 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
- ca.spottedleaf.concurrentutil.util.Priority priority,
- java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-- List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
-- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
-- ServerChunkCache chunkProvider = this.getChunkSource();
-+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - rewrite chunk system
-+ }
-+ // Paper end
-+
-+ // Paper start - optimise getPlayerByUUID
-+ @Nullable
-+ @Override
-+ public Player getPlayerByUUID(UUID uuid) {
-+ final Player player = this.getServer().getPlayerList().getPlayer(uuid);
-+ return player != null && player.level() == this ? player : null;
-+ }
-+ // Paper end - optimise getPlayerByUUID
-+ // Paper start - rewrite chunk system
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this);
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController;
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController;
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController;
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
-+ 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) {
-+ return this.chunkSource.getChunkNow(chunkX, chunkZ);
-+ }
-+
-+ @Override
-+ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ if (newChunkHolder == null) {
-+ return null;
-+ }
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
-+ return lastCompletion == null ? null : lastCompletion.chunk();
-+ }
-
-- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
-- int[] loadedChunks = new int[1];
-+ @Override
-+ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-+ if (newChunkHolder == null) {
-+ return null;
-+ }
-+ return newChunkHolder.getChunkIfPresentUnchecked(leastStatus);
-+ }
-+
-+ @Override
-+ public final void moonrise$midTickTasks() {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
-+ }
-+
-+ @Override
-+ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
-+ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
-+ }
-
-- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
-+ return this.chunkTaskScheduler;
-+ }
-
-- java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController() {
-+ return this.chunkDataController;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController() {
-+ return this.poiDataController;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController() {
-+ return this.entityDataController;
-+ }
-+
-+ @Override
-+ public final int moonrise$getRegionChunkShift() {
-+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() {
-+ return this.chunkLoader;
-+ }
-+
-+ @Override
-+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final ca.spottedleaf.concurrentutil.util.Priority priority,
-+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ this.moonrise$loadChunksAsync(
-+ (pos.getX() - radiusBlocks) >> 4,
-+ (pos.getX() + radiusBlocks) >> 4,
-+ (pos.getZ() - radiusBlocks) >> 4,
-+ (pos.getZ() + radiusBlocks) >> 4,
-+ priority, onLoad
-+ );
-+ }
-+
-+ @Override
-+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority,
-+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ this.moonrise$loadChunksAsync(
-+ (pos.getX() - radiusBlocks) >> 4,
-+ (pos.getX() + radiusBlocks) >> 4,
-+ (pos.getZ() - radiusBlocks) >> 4,
-+ (pos.getZ() + radiusBlocks) >> 4,
-+ chunkStatus, priority, onLoad
-+ );
-+ }
-+
-+ @Override
-+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
-+ final ca.spottedleaf.concurrentutil.util.Priority priority,
-+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
-+ }
-+
-+ @Override
-+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
-+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority,
-+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
-+
-+ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
-+ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
-+ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
-+ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
-+
-+ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
-+
-+ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
- if (chunk != null) {
-- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
-- ret.add(chunk);
-- ticketLevels.add(ticketLevel);
-- chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
-+ synchronized (ret) {
-+ ret.add(chunk);
-+ }
-+ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier);
- }
-- if (++loadedChunks[0] == requiredChunks) {
-+ if (loadedChunks.incrementAndGet() == requiredChunks) {
- try {
- onLoad.accept(java.util.Collections.unmodifiableList(ret));
- } finally {
- for (int i = 0, len = ret.size(); i < len; ++i) {
-- ChunkPos chunkPos = ret.get(i).getPos();
-- int ticketLevel = ticketLevels.getInt(i);
-+ final ChunkPos chunkPos = ret.get(i).getPos();
-
-- chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
-- chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
-+ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier);
- }
- }
- }
-@@ -330,22 +452,137 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
- for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-- ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
-- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
-- );
-+ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer);
- }
- }
- }
-- // Paper end
-
-- // Paper start - optimise getPlayerByUUID
-- @Nullable
- @Override
-- public Player getPlayerByUUID(UUID uuid) {
-- final Player player = this.getServer().getPlayerList().getPlayer(uuid);
-- return player != null && player.level() == this ? player : null;
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
-+ return this.viewDistanceHolder;
- }
-- // Paper end - optimise getPlayerByUUID
-+
-+ @Override
-+ public final long moonrise$getLastMidTickFailure() {
-+ return this.lastMidTickFailure;
-+ }
-+
-+ @Override
-+ public final void moonrise$setLastMidTickFailure(final long time) {
-+ this.lastMidTickFailure = time;
-+ }
-+
-+ @Override
-+ 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;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ final ServerChunkCache chunkSource = this.chunkSource;
-+
-+ for (int currZ = fromZ; currZ <= toZ; ++currZ) {
-+ for (int currX = fromX; currX <= toX; ++currX) {
-+ if (!chunkSource.hasChunk(currX, currZ)) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - chunk tick iteration
-+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
-+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS);
-+ private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap();
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks() {
-+ return this.playerTickingChunks;
-+ }
-+
-+ @Override
-+ public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) {
-+ final ChunkPos pos = chunk.getPos();
-+ if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) {
-+ return;
-+ }
-+
-+ this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
-+ }
-+
-+ @Override
-+ public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) {
-+ this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
-+ }
-+
-+ @Override
-+ public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) {
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async");
-+
-+ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) {
-+ // already added
-+ return;
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
-+ .chunkHolderManager.getChunkHolder(chunkKey);
-+
-+ if (chunkHolder == null || !chunkHolder.isTickingReady()) {
-+ return;
-+ }
-+
-+ this.playerTickingChunks.add(
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
-+ );
-+ }
-+
-+ @Override
-+ public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) {
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async");
-+
-+ final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final int val = this.playerTickingRequests.addTo(chunkKey, -1);
-+
-+ if (val <= 0) {
-+ throw new IllegalStateException("Negative counter");
-+ }
-+
-+ if (val != 1) {
-+ // still has at least one request
-+ return;
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
-+ .chunkHolderManager.getChunkHolder(chunkKey);
-+
-+ if (chunkHolder == null || !chunkHolder.isTickingReady()) {
-+ return;
-+ }
-+
-+ this.playerTickingChunks.remove(
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
-+ );
-+ }
-+ // Paper end - chunk tick iteration
-
- // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
- public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
-@@ -379,14 +616,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- DataFixer datafixer = minecraftserver.getFixerUpper();
- EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
-
-- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
-+ // Paper - rewrite chunk system
- StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
- int j = this.spigotConfig.viewDistance; // Spigot
- int k = this.spigotConfig.simulationDistance; // Spigot
-- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-+ // Paper - rewrite chunk system
-
-- Objects.requireNonNull(this.entityManager);
-- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
-+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
- return minecraftserver.overworld().getDataStorage();
- });
- this.chunkSource.getGeneratorState().ensureStructuresGenerated();
-@@ -414,6 +650,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
- return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
- });
-+ // Paper start - rewrite chunk system
-+ this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
-+ this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this);
-+ this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
-+ new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
-+ new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"),
-+ convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"),
-+ minecraftserver.forceSynchronousWrites()
-+ ),
-+ this.chunkTaskScheduler
-+ );
-+ this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this, this.chunkTaskScheduler);
-+ this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler);
-+ // Paper end - rewrite chunk system
- this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
- }
-
-@@ -536,7 +786,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- gameprofilerfiller.push("checkDespawn");
- entity.checkDespawn();
- gameprofilerfiller.pop();
-- if (entity instanceof ServerPlayer || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
-+ if (true) { // Paper - rewrite chunk system
- Entity entity1 = entity.getVehicle();
-
- if (entity1 != null) {
-@@ -559,13 +809,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- }
-
- gameprofilerfiller.push("entityManagement");
-- this.entityManager.tick();
-+ // Paper - rewrite chunk system
- gameprofilerfiller.pop();
- }
-
- @Override
- public boolean shouldTickBlocksAt(long chunkPos) {
-- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
-+ return holder != null && holder.isTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- protected void tickTime() {
-@@ -605,7 +858,60 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- });
- }
-
-+ // Paper start - optimise random ticking
-+ private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed());
-+
-+ private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) {
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this);
-+ final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom;
-+ final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
-+
-+ final ChunkPos cpos = chunk.getPos();
-+ final int offsetX = cpos.x << 4;
-+ final int offsetZ = cpos.z << 4;
-+
-+ for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) {
-+ final int offsetY = (sectionIndex + minSection) << 4;
-+ final LevelChunkSection section = sections[sectionIndex];
-+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> states = section.states;
-+ if (!section.isRandomlyTickingBlocks()) {
-+ continue;
-+ }
-+
-+ final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList();
-+
-+ for (int i = 0; i < tickSpeed; ++i) {
-+ final int tickingBlocks = tickList.size();
-+ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1);
-+
-+ if (index >= tickingBlocks) {
-+ // most of the time we fall here
-+ continue;
-+ }
-+
-+ final int location = (int)tickList.getRaw(index) & 0xFFFF;
-+ final BlockState state = states.get(location);
-+
-+ // do not use a mutable pos, as some random tick implementations store the input without calling immutable()!
-+ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ);
-+
-+ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
-+ if (doubleTickFluids) {
-+ final FluidState fluidState = state.getFluidState();
-+ if (fluidState.isRandomlyTicking()) {
-+ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
-+ }
-+ }
-+ }
-+ }
-+
-+ return;
-+ }
-+ // Paper end - optimise random ticking
-+
- public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
-+ final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
- ChunkPos chunkcoordintpair = chunk.getPos();
- boolean flag = this.isRaining();
- int j = chunkcoordintpair.getMinBlockX();
-@@ -613,7 +919,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- ProfilerFiller gameprofilerfiller = Profiler.get();
-
- gameprofilerfiller.push("thunder");
-- if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
-+ if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking
- BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
-
- if (this.isRainingAt(blockposition)) {
-@@ -645,7 +951,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
- for (int l = 0; l < randomTickSpeed; ++l) {
-- if (this.random.nextInt(48) == 0) {
-+ if (simpleRandom.nextInt(48) == 0) { // Paper - optimise random ticking
- this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15));
- }
- }
-@@ -653,35 +959,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- gameprofilerfiller.popPush("tickBlocks");
- if (randomTickSpeed > 0) {
-- LevelChunkSection[] achunksection = chunk.getSections();
--
-- for (int i1 = 0; i1 < achunksection.length; ++i1) {
-- LevelChunkSection chunksection = achunksection[i1];
--
-- if (chunksection.isRandomlyTicking()) {
-- int j1 = chunk.getSectionYFromSectionIndex(i1);
-- int k1 = SectionPos.sectionToBlockCoord(j1);
--
-- for (int l1 = 0; l1 < randomTickSpeed; ++l1) {
-- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15);
--
-- gameprofilerfiller.push("randomTick");
-- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k);
--
-- if (iblockdata.isRandomlyTicking()) {
-- iblockdata.randomTick(this, blockposition1, this.random);
-- }
--
-- FluidState fluid = iblockdata.getFluidState();
--
-- if (fluid.isRandomlyTicking()) {
-- fluid.randomTick(this, blockposition1, this.random);
-- }
--
-- gameprofilerfiller.pop();
-- }
-- }
-- }
-+ this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking
- }
-
- gameprofilerfiller.pop();
-@@ -954,6 +1232,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- if (fluid1.is(fluid)) {
- fluid1.tick(this, pos, iblockdata);
- }
-+ // Paper start - rewrite chunk system
-+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
-+ }
-+ // Paper end - rewrite chunk system
-
- }
-
-@@ -963,6 +1246,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- if (iblockdata.is(block)) {
- iblockdata.tick(this, pos, this.random);
- }
-+ // Paper start - rewrite chunk system
-+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
-+ }
-+ // Paper end - rewrite chunk system
-
- }
-
-@@ -1041,6 +1329,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- }
-
- public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
-+ // Paper start - add close param
-+ this.save(progressListener, flush, savingDisabled, false);
-+ }
-+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) {
-+ // Paper end - add close param
- ServerChunkCache chunkproviderserver = this.getChunkSource();
-
- if (!savingDisabled) {
-@@ -1054,14 +1347,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- progressListener.progressStage(Component.translatable("menu.savingChunks"));
- }
-
-- chunkproviderserver.save(flush);
-- if (flush) {
-- this.entityManager.saveAll();
-- } else {
-- this.entityManager.autoSave();
-- }
-+ if (!close) { chunkproviderserver.save(flush); } // Paper - add close param
-+ // Paper - rewrite chunk system
-
- }
-+ // Paper start - add close param
-+ if (close) {
-+ try {
-+ chunkproviderserver.close(!savingDisabled);
-+ } catch (IOException never) {
-+ throw new RuntimeException(never);
-+ }
-+ }
-+ // Paper end - add close param
-
- // CraftBukkit start - moved from MinecraftServer.saveChunks
- ServerLevel worldserver1 = this;
-@@ -1201,7 +1499,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
- }
-
-- this.entityManager.addNewEntity(player);
-+ this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system
- }
-
- // CraftBukkit start
-@@ -1232,7 +1530,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- }
- // CraftBukkit end
-
-- return this.entityManager.addNewEntity(entity);
-+ return this.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system
- }
- }
-
-@@ -1243,11 +1541,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
- // CraftBukkit end
-- Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
-- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
--
-- Objects.requireNonNull(this.entityManager);
-- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
-+ if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
- return false;
- } else {
- this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
-@@ -1924,7 +2218,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- }
- }
-
-- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
-+ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
- bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
- bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
- bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
-@@ -1973,7 +2267,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
-
- try {
-- playerchunkmap.dumpChunks(bufferedwriter2);
-+ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system
- } catch (Throwable throwable4) {
- if (bufferedwriter2 != null) {
- try {
-@@ -1994,7 +2288,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
-
- try {
-- this.entityManager.dumpSections(bufferedwriter3);
-+ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system
- } catch (Throwable throwable6) {
- if (bufferedwriter3 != null) {
- try {
-@@ -2136,7 +2430,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
-
- @VisibleForTesting
- public String getWatchdogStats() {
-- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> {
-+ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system
- return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
- }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats());
- }
-@@ -2166,15 +2460,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- @Override
- public LevelEntityGetter<Entity> getEntities() {
- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
-- return this.entityManager.getEntityGetter();
-+ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
- }
-
- public void addLegacyChunkEntities(Stream<Entity> entities) {
-- this.entityManager.addLegacyChunkEntities(entities);
-+ // Paper start - add chunkpos param
-+ this.addLegacyChunkEntities(entities, null);
-+ }
-+ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
-+ // Paper end - add chunkpos param
-+ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
- }
-
- public void addWorldGenChunkEntities(Stream<Entity> entities) {
-- this.entityManager.addWorldGenChunkEntities(entities);
-+ // Paper start - add chunkpos param
-+ this.addWorldGenChunkEntities(entities, null);
-+ }
-+ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
-+ // Paper end - add chunkpos param
-+ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
- }
-
- public void startTickingChunk(LevelChunk chunk) {
-@@ -2194,34 +2498,47 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- @Override
- public void close() throws IOException {
- super.close();
-- this.entityManager.close();
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public String gatherChunkSourceStats() {
- String s = this.chunkSource.gatherStats();
-
-- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats();
-+ return "Chunks[S] W: " + s + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
- }
-
- public boolean areEntitiesLoaded(long chunkPos) {
-- return this.entityManager.areEntitiesLoaded(chunkPos);
-+ return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
- }
-
- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
-- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
-+ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
-+ return chunkHolder != null && chunkHolder.isTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- public boolean isPositionEntityTicking(BlockPos pos) {
-- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos));
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
-+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- public boolean isNaturalSpawningAllowed(BlockPos pos) {
-- return this.entityManager.canPositionTick(pos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
-+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- public boolean isNaturalSpawningAllowed(ChunkPos pos) {
-- return this.entityManager.canPositionTick(pos);
-+ // Paper start - rewrite chunk system
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
-+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
-+ // Paper end - rewrite chunk system
- }
-
- @Override
-@@ -2277,7 +2594,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
-
- crashreportsystemdetails.setDetail("Loaded entity count", () -> {
-- return String.valueOf(this.entityManager.count());
-+ return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system
- });
- return crashreportsystemdetails;
- }
-diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
-index f6e3073e1f1ff99f6917d84974a18e3e756fa9ea..ba873bcc183f9b3f64ba39be08cb88a95ff52b0e 100644
---- a/net/minecraft/server/level/ServerPlayer.java
-+++ b/net/minecraft/server/level/ServerPlayer.java
-@@ -217,7 +217,7 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
- import org.bukkit.inventory.MainHand;
- // CraftBukkit end
-
--public class ServerPlayer extends net.minecraft.world.entity.player.Player {
-+public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
-
- private static final Logger LOGGER = LogUtils.getLogger();
- private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
-@@ -322,6 +322,36 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
- public @Nullable String clientBrandName = null; // Paper - Brand support
- public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
-
-+ // Paper start - rewrite chunk system
-+ private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
-+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
-+
-+ @Override
-+ public final boolean moonrise$isRealPlayer() {
-+ return this.isRealPlayer;
-+ }
-+
-+ @Override
-+ public final void moonrise$setRealPlayer(final boolean real) {
-+ this.isRealPlayer = real;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() {
-+ return this.chunkLoader;
-+ }
-+
-+ @Override
-+ public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) {
-+ this.chunkLoader = loader;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
-+ return this.viewDistanceHolder;
-+ }
-+ // Paper end - rewrite chunk system
-+
- public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
- super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
- this.chatVisibility = ChatVisiblity.FULL;
-diff --git a/net/minecraft/server/level/ThreadedLevelLightEngine.java b/net/minecraft/server/level/ThreadedLevelLightEngine.java
-index 39d34f3728ae8d845d1bffc09f3ab8b64eb4d48b..3e82adf061bd0ec0100ca4d16ec9b157bddf99a7 100644
---- a/net/minecraft/server/level/ThreadedLevelLightEngine.java
-+++ b/net/minecraft/server/level/ThreadedLevelLightEngine.java
-@@ -22,23 +22,134 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
- import net.minecraft.world.level.lighting.LevelLightEngine;
- import org.slf4j.Logger;
-
--public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
-+public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system
- public static final int DEFAULT_BATCH_SIZE = 1000;
- private static final Logger LOGGER = LogUtils.getLogger();
-- private final ConsecutiveExecutor consecutiveExecutor;
-- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
-+ // Paper - rewrite chunk sytem
- private final ChunkMap chunkMap;
-- private final ChunkTaskDispatcher taskDispatcher;
-+ // Paper - rewrite chunk sytem
- private final int taskPerBatch = 1000;
-- private final AtomicBoolean scheduled = new AtomicBoolean();
-+ // Paper - rewrite chunk sytem
-+
-+ // Paper start - rewrite chunk system
-+ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong();
-+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) {
-+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
-+
-+ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ);
-+ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // do not accept updates in unlit chunks, unless we might be generating a chunk
-+ return;
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get();
-+
-+ if (scheduledTask == null) {
-+ // not scheduled
-+ return;
-+ }
-+
-+ if (!scheduledTask.markTicketAdded()) {
-+ // ticket already added
-+ return;
-+ }
-+
-+ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement());
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
-+
-+ scheduledTask.queueOrRunTask(() -> {
-+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
-+ });
-+ }
-+
-+ @Override
-+ public final int starlight$serverRelightChunks(final java.util.Collection<net.minecraft.world.level.ChunkPos> chunks0,
-+ final java.util.function.Consumer<net.minecraft.world.level.ChunkPos> chunkLightCallback,
-+ final java.util.function.IntConsumer onComplete) {
-+ final java.util.Set<net.minecraft.world.level.ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks0);
-+ final java.util.Map<net.minecraft.world.level.ChunkPos, Long> ticketIds = new java.util.HashMap<>();
-+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
-+
-+ for (final java.util.Iterator<net.minecraft.world.level.ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
-+ final ChunkPos pos = iterator.next();
-+
-+ final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId();
-+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
-+ ticketIds.put(pos, id);
-+
-+ final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ ticketIds.remove(pos);
-+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
-+ continue;
-+ }
-+ }
-+
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> {
-+ ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks(
-+ chunks,
-+ (final ChunkPos pos) -> {
-+ if (chunkLightCallback != null) {
-+ chunkLightCallback.accept(pos);
-+ }
-+
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(
-+ pos.x, pos.z
-+ );
-+
-+ if (chunkHolder == null) {
-+ return;
-+ }
-+
-+ final java.util.List<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false);
-+
-+ if (players.isEmpty()) {
-+ return;
-+ }
-+
-+ final net.minecraft.network.protocol.Packet<?> relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(
-+ pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this,
-+ null, null
-+ );
-+
-+ for (final ServerPlayer player : players) {
-+ final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection;
-+ if (conn != null) {
-+ conn.send(relightPacket);
-+ }
-+ }
-+ });
-+ },
-+ (final int relight) -> {
-+ if (onComplete != null) {
-+ onComplete.accept(relight);
-+ }
-+
-+ for (final java.util.Map.Entry<ChunkPos, Long> entry : ticketIds.entrySet()) {
-+ world.getChunkSource().removeRegionTicket(
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(),
-+ ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue()
-+ );
-+ }
-+ }
-+ );
-+ });
-+
-+ return chunks.size();
-+ }
-+ // Paper end - rewrite chunk system
-
- public ThreadedLevelLightEngine(
- LightChunkGetter chunkProvider, ChunkMap chunkLoadingManager, boolean hasBlockLight, ConsecutiveExecutor processor, ChunkTaskDispatcher executor
- ) {
- super(chunkProvider, true, hasBlockLight);
- this.chunkMap = chunkLoadingManager;
-- this.taskDispatcher = executor;
-- this.consecutiveExecutor = processor;
-+ // Paper - rewrite chunk sytem
- }
-
- @Override
-@@ -52,164 +163,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void checkBlock(BlockPos pos) {
-- BlockPos blockPos = pos.immutable();
-- this.addTask(
-- SectionPos.blockToSectionCoord(pos.getX()),
-- SectionPos.blockToSectionCoord(pos.getZ()),
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
-- );
-+ // Paper start - rewrite chunk system
-+ final BlockPos posCopy = pos.immutable();
-+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
-+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy);
-+ });
-+ // Paper end - rewrite chunk system
- }
-
- protected void updateChunkStatus(ChunkPos pos) {
-- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-- super.retainData(pos, false);
-- super.setLightEnabled(pos, false);
--
-- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) {
-- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null);
-- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null);
-- }
--
-- for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) {
-- super.updateSectionStatus(SectionPos.of(pos, j), true);
-- }
-- }, () -> "updateChunkStatus " + pos + " true"));
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public void updateSectionStatus(SectionPos pos, boolean notReady) {
-- this.addTask(
-- pos.x(),
-- pos.z(),
-- () -> 0,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
-- );
-+ // Paper start - rewrite chunk system
-+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
-+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady);
-+ });
-+ // Paper end - rewrite chunk system
- }
-
- @Override
- public void propagateLightSources(ChunkPos chunkPos) {
-- this.addTask(
-- chunkPos.x,
-- chunkPos.z,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos)
-- );
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public void setLightEnabled(ChunkPos pos, boolean retainData) {
-- this.addTask(
-- pos.x,
-- pos.z,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData)
-- );
-+ // Paper start - rewrite chunk system
- }
-
- @Override
- public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
-- this.addTask(
-- pos.x(),
-- pos.z(),
-- () -> 0,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos)
-- );
-+ // Paper start - rewrite chunk system
- }
-
- private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
-- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
-- this.taskDispatcher.submit(() -> {
-- this.lightTasks.add(Pair.of(stage, task));
-- if (this.lightTasks.size() >= 1000) {
-- this.runUpdate();
-- }
-- }, ChunkPos.asLong(x, z), completedLevelSupplier);
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- @Override
- public void retainData(ChunkPos pos, boolean retainData) {
-- this.addTask(
-- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
-- );
-+ // Paper start - rewrite chunk system
- }
-
- public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
-- ChunkPos chunkPos = chunk.getPos();
-- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-- LevelChunkSection[] levelChunkSections = chunk.getSections();
--
-- for (int i = 0; i < chunk.getSectionsCount(); i++) {
-- LevelChunkSection levelChunkSection = levelChunkSections[i];
-- if (!levelChunkSection.hasOnlyAir()) {
-- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
-- super.updateSectionStatus(SectionPos.of(chunkPos, j), false);
-- }
-- }
-- }, () -> "initializeLight: " + chunkPos));
-- return CompletableFuture.supplyAsync(() -> {
-- super.setLightEnabled(chunkPos, bl);
-- super.retainData(chunkPos, false);
-- return chunk;
-- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
-+ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system
- }
-
- public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
-- ChunkPos chunkPos = chunk.getPos();
-- chunk.setLightCorrect(false);
-- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-- if (!excludeBlocks) {
-- super.propagateLightSources(chunkPos);
-- }
-- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks));
-- return CompletableFuture.supplyAsync(() -> {
-- chunk.setLightCorrect(true);
-- return chunk;
-- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void tryScheduleUpdate() {
-- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
-- this.consecutiveExecutor.schedule(() -> {
-- this.runUpdate();
-- this.scheduled.set(false);
-- });
-- }
-+ // Paper - rewrite chunk system
- }
-
- private void runUpdate() {
-- int i = Math.min(this.lightTasks.size(), 1000);
-- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
--
-- int j;
-- for (j = 0; objectListIterator.hasNext() && j < i; j++) {
-- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
-- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
-- pair.getSecond().run();
-- }
-- }
--
-- objectListIterator.back(j);
-- super.runLightUpdates();
--
-- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
-- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
-- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
-- pair2.getSecond().run();
-- }
--
-- objectListIterator.remove();
-- }
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public CompletableFuture<?> waitForPendingTasks(int x, int z) {
-- return CompletableFuture.runAsync(() -> {
-- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback));
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- static enum TaskType {
-diff --git a/net/minecraft/server/level/Ticket.java b/net/minecraft/server/level/Ticket.java
-index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644
---- a/net/minecraft/server/level/Ticket.java
-+++ b/net/minecraft/server/level/Ticket.java
-@@ -2,13 +2,25 @@ package net.minecraft.server.level;
-
- import java.util.Objects;
-
--public final class Ticket<T> implements Comparable<Ticket<?>> {
-+public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system
- private final TicketType<T> type;
- private final int ticketLevel;
- public final T key;
-- private long createdTick;
-+ // Paper start - rewrite chunk system
-+ private long removeDelay;
-
-- protected Ticket(TicketType<T> type, int level, T argument) {
-+ @Override
-+ public final long moonrise$getRemoveDelay() {
-+ return this.removeDelay;
-+ }
-+
-+ @Override
-+ public final void moonrise$setRemoveDelay(final long removeDelay) {
-+ this.removeDelay = removeDelay;
-+ }
-+ // Paper end - rewerite chunk system
-+
-+ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public
- this.type = type;
- this.ticketLevel = level;
- this.key = argument;
-@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
-
- @Override
- public String toString() {
-- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick;
-+ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system
- }
-
- public TicketType<T> getType() {
-@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
- }
-
- protected void setCreatedTick(long tickCreated) {
-- this.createdTick = tickCreated;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- protected boolean timedOut(long currentTick) {
-- long l = this.type.timeout();
-- return l != 0L && currentTick - this.createdTick > l;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
- }
-diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java
-index b7d29389a357f142237cecd75f8ca91cf1eb6b5b..e4b0dc3121101d54394a0c3a413dabf8103b2ea6 100644
---- a/net/minecraft/server/level/WorldGenRegion.java
-+++ b/net/minecraft/server/level/WorldGenRegion.java
-@@ -85,6 +85,36 @@ public class WorldGenRegion implements WorldGenLevel {
- private final AtomicLong subTickCount = new AtomicLong();
- private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random");
-
-+ // Paper start - rewrite chunk system
-+ /**
-+ * During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla
-+ * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate.
-+ * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate
-+ * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished
-+ * lighting.
-+ */
-+ @Override
-+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
-+ }
-+
-+ /**
-+ * See above
-+ */
-+ @Override
-+ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getRawBrightness(blockPos, subtract);
-+ }
-+ // Paper end - rewrite chunk system
-+
- public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) {
- this.generatingStep = generationStep;
- this.cache = chunks;
-diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
-index c68040a59fa8aa9b8b9f1e0b4fdded565ea592d9..7913c41aac1f9dd53a2b49da2a17fd894bcb6b3a 100644
---- a/net/minecraft/server/players/PlayerList.java
-+++ b/net/minecraft/server/players/PlayerList.java
-@@ -1426,7 +1426,7 @@ public abstract class PlayerList {
-
- public void setViewDistance(int viewDistance) {
- this.viewDistance = viewDistance;
-- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance));
-+ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - rewrite chunk system
- Iterator iterator = this.server.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
-@@ -1441,7 +1441,7 @@ public abstract class PlayerList {
-
- public void setSimulationDistance(int simulationDistance) {
- this.simulationDistance = simulationDistance;
-- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance));
-+ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - rewrite chunk system
- Iterator iterator = this.server.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
-diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java
-index 68648c5a5e3ff079f832092af0f2f801c42d1ede..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644
---- a/net/minecraft/util/BitStorage.java
-+++ b/net/minecraft/util/BitStorage.java
-@@ -2,7 +2,7 @@ package net.minecraft.util;
-
- import java.util.function.IntConsumer;
-
--public interface BitStorage {
-+public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage { // Paper - block counting
- int getAndSet(int index, int value);
-
- void set(int index, int value);
-@@ -20,4 +20,22 @@ public interface BitStorage {
- void unpack(int[] out);
-
- BitStorage copy();
-+
-+ // Paper start - block counting
-+ // provide default impl in case mods implement this...
-+ @Override
-+ public default it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() {
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>();
-+
-+ final int size = this.getSize();
-+ for (int index = 0; index < size; ++index) {
-+ final int paletteIdx = this.get(index);
-+ ret.computeIfAbsent(paletteIdx, (final int key) -> {
-+ return new it.unimi.dsi.fastutil.shorts.ShortArrayList();
-+ }).add((short)index);
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - block counting
- }
-diff --git a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644
---- a/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-+++ b/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-@@ -7,7 +7,7 @@ import java.util.Iterator;
- import javax.annotation.Nullable;
- import net.minecraft.core.IdMap;
-
--public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
-+public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<K> { // Paper - optimise palette reads
- private static final int NOT_FOUND = -1;
- private static final Object EMPTY_SLOT = null;
- private static final float LOADFACTOR = 0.8F;
-@@ -17,6 +17,16 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
- private int nextId;
- private int size;
-
-+ // Paper start - optimise palette reads
-+ private ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> reference;
-+
-+ @Override
-+ public final K[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> src) {
-+ this.reference = src;
-+ return this.byId;
-+ }
-+ // Paper end - optimise palette reads
-+
- private CrudeIncrementalIntIdentityHashBiMap(int size) {
- this.keys = (K[])(new Object[size]);
- this.values = new int[size];
-@@ -88,6 +98,12 @@ public class CrudeIncrementalIntIdentityHashBiMap<K> implements IdMap<K> {
- this.byId = crudeIncrementalIntIdentityHashBiMap.byId;
- this.nextId = crudeIncrementalIntIdentityHashBiMap.nextId;
- this.size = crudeIncrementalIntIdentityHashBiMap.size;
-+ // Paper start - optimise palette reads
-+ final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<K> ref = this.reference;
-+ if (ref != null) {
-+ ref.moonrise$setPalette(this.byId);
-+ }
-+ // Paper end - optimise palette reads
- }
-
- public void addMapping(K value, int id) {
-diff --git a/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java
-index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..d99ec470b4653beab630999a5b2c1a6428b20c38 100644
---- a/net/minecraft/util/SimpleBitStorage.java
-+++ b/net/minecraft/util/SimpleBitStorage.java
-@@ -208,6 +208,20 @@ public class SimpleBitStorage implements BitStorage {
- private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage
- private final int divideShift;
-
-+ // Paper start - optimise bitstorage read/write operations
-+ private static final int[] BETTER_MAGIC = new int[33];
-+ static {
-+ // 20 bits of precision
-+ // since index is always [0, 4095] (i.e 12 bits), multiplication by a magic value here (20 bits)
-+ // fits exactly in an int and allows us to use integer arithmetic
-+ for (int bits = 1; bits < BETTER_MAGIC.length; ++bits) {
-+ BETTER_MAGIC[bits] = (int)ca.spottedleaf.concurrentutil.util.IntegerUtil.getUnsignedDivisorMagic(64L / bits, 20);
-+ }
-+ }
-+ private final int magic;
-+ private final int mulBits;
-+ // Paper end - optimise bitstorage read/write operations
-+
- public SimpleBitStorage(int elementBits, int size, int[] data) {
- this(elementBits, size);
- int i = 0;
-@@ -261,6 +275,13 @@ public class SimpleBitStorage implements BitStorage {
- } else {
- this.data = new long[j];
- }
-+ // Paper start - optimise bitstorage read/write operations
-+ this.magic = BETTER_MAGIC[this.bits];
-+ this.mulBits = (64 / this.bits) * this.bits;
-+ if (this.size > 4096) {
-+ throw new IllegalStateException("Size > 4096 not supported");
-+ }
-+ // Paper end - optimise bitstorage read/write operations
- }
-
- private int cellIndex(int index) {
-@@ -273,31 +294,54 @@ public class SimpleBitStorage implements BitStorage {
- public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
- //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
- //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
-- int i = this.cellIndex(index);
-- long l = this.data[i];
-- int j = (index - i * this.valuesPerLong) * this.bits;
-- int k = (int)(l >> j & this.mask);
-- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j;
-- return k;
-+ // Paper start - optimise bitstorage read/write operations
-+ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
-+ final int divQ = full >>> 20;
-+ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20;
-+
-+ final long[] dataArray = this.data;
-+
-+ final long data = dataArray[divQ];
-+ final long mask = this.mask;
-+
-+ final long write = data & ~(mask << divR) | ((long)value & mask) << divR;
-+
-+ dataArray[divQ] = write;
-+
-+ return (int)(data >>> divR & mask);
-+ // Paper end - optimise bitstorage read/write operations
- }
-
- @Override
- public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
- //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
- //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
-- int i = this.cellIndex(index);
-- long l = this.data[i];
-- int j = (index - i * this.valuesPerLong) * this.bits;
-- this.data[i] = l & ~(this.mask << j) | ((long)value & this.mask) << j;
-+ // Paper start - optimise bitstorage read/write operations
-+ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
-+ final int divQ = full >>> 20;
-+ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20;
-+
-+ final long[] dataArray = this.data;
-+
-+ final long data = dataArray[divQ];
-+ final long mask = this.mask;
-+
-+ final long write = data & ~(mask << divR) | ((long)value & mask) << divR;
-+
-+ dataArray[divQ] = write;
-+ // Paper end - optimise bitstorage read/write operations
- }
-
- @Override
- public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
- //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
-- int i = this.cellIndex(index);
-- long l = this.data[i];
-- int j = (index - i * this.valuesPerLong) * this.bits;
-- return (int)(l >> j & this.mask);
-+ // Paper start - optimise bitstorage read/write operations
-+ final int full = this.magic * index; // 20 bits of magic + 12 bits of index = barely int
-+ final int divQ = full >>> 20;
-+ final int divR = (full & 0xFFFFF) * this.mulBits >>> 20;
-+
-+ return (int)(this.data[divQ] >>> divR & this.mask);
-+ // Paper end - optimise bitstorage read/write operations
- }
-
- @Override
-@@ -362,6 +406,67 @@ public class SimpleBitStorage implements BitStorage {
- return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone());
- }
-
-+ // Paper start - block counting
-+ @Override
-+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() {
-+ final int valuesPerLong = this.valuesPerLong;
-+ final int bits = this.bits;
-+ final long mask = (1L << bits) - 1L;
-+ final int size = this.size;
-+
-+ if (bits <= 6) {
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList[] byId = new it.unimi.dsi.fastutil.shorts.ShortArrayList[1 << bits];
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1 << bits);
-+
-+ int index = 0;
-+
-+ for (long value : this.data) {
-+ int li = 0;
-+ do {
-+ final int paletteIdx = (int)(value & mask);
-+ value >>= bits;
-+ ++li;
-+
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coords = byId[paletteIdx];
-+ if (coords != null) {
-+ coords.add((short)index++);
-+ continue;
-+ } else {
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList newCoords = new it.unimi.dsi.fastutil.shorts.ShortArrayList(64);
-+ byId[paletteIdx] = newCoords;
-+ newCoords.add((short)index++);
-+ ret.put(paletteIdx, newCoords);
-+ continue;
-+ }
-+ } while (li < valuesPerLong && index < size);
-+ }
-+
-+ return ret;
-+ } else {
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(
-+ 1 << 6
-+ );
-+
-+ int index = 0;
-+
-+ for (long value : this.data) {
-+ int li = 0;
-+ do {
-+ final int paletteIdx = (int)(value & mask);
-+ value >>= bits;
-+ ++li;
-+
-+ ret.computeIfAbsent(paletteIdx, (final int key) -> {
-+ return new it.unimi.dsi.fastutil.shorts.ShortArrayList(64);
-+ }).add((short)index++);
-+ } while (li < valuesPerLong && index < size);
-+ }
-+
-+ return ret;
-+ }
-+ }
-+ // Paper end - block counting
-+
- public static class InitializationException extends RuntimeException {
- InitializationException(String message) {
- super(message);
-diff --git a/net/minecraft/util/SortedArraySet.java b/net/minecraft/util/SortedArraySet.java
-index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644
---- a/net/minecraft/util/SortedArraySet.java
-+++ b/net/minecraft/util/SortedArraySet.java
-@@ -8,12 +8,89 @@ import java.util.Iterator;
- import java.util.NoSuchElementException;
- import javax.annotation.Nullable;
-
--public class SortedArraySet<T> extends AbstractSet<T> {
-+public class SortedArraySet<T> extends AbstractSet<T> implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet<T> { // Paper - rewrite chunk system
- private static final int DEFAULT_INITIAL_CAPACITY = 10;
- private final Comparator<T> comparator;
- T[] contents;
- int size;
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final boolean removeIf(final java.util.function.Predicate<? super T> filter) {
-+ // prev. impl used an iterator, which could be n^2 and creates garbage
-+ int i = 0;
-+ final int len = this.size;
-+ final T[] backingArray = this.contents;
-+
-+ for (;;) {
-+ if (i >= len) {
-+ return false;
-+ }
-+ if (!filter.test(backingArray[i])) {
-+ ++i;
-+ continue;
-+ }
-+ break;
-+ }
-+
-+ // we only want to write back to backingArray if we really need to
-+
-+ int lastIndex = i; // this is where new elements are shifted to
-+
-+ for (; i < len; ++i) {
-+ final T curr = backingArray[i];
-+ if (!filter.test(curr)) { // if test throws we're screwed
-+ backingArray[lastIndex++] = curr;
-+ }
-+ }
-+
-+ // cleanup end
-+ Arrays.fill(backingArray, lastIndex, len, null);
-+ this.size = lastIndex;
-+ return true;
-+ }
-+
-+ @Override
-+ public final T moonrise$replace(final T object) {
-+ final int index = this.findIndex(object);
-+ if (index >= 0) {
-+ final T old = this.contents[index];
-+ this.contents[index] = object;
-+ return old;
-+ } else {
-+ this.addInternal(object, getInsertionPosition(index));
-+ return object;
-+ }
-+ }
-+
-+ @Override
-+ public final T moonrise$removeAndGet(final T object) {
-+ int i = this.findIndex(object);
-+ if (i >= 0) {
-+ final T ret = this.contents[i];
-+ this.removeInternal(i);
-+ return ret;
-+ } else {
-+ return null;
-+ }
-+ }
-+
-+ @Override
-+ public final SortedArraySet<T> moonrise$copy() {
-+ final SortedArraySet<T> ret = SortedArraySet.create(this.comparator, 0);
-+
-+ ret.size = this.size;
-+ ret.contents = Arrays.copyOf(this.contents, this.size);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public Object[] moonrise$copyBackingArray() {
-+ return this.contents.clone();
-+ }
-+ // Paper end - rewrite chunk system
-+
- private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
- this.comparator = comparator;
- if (initialCapacity < 0) {
-diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java
-index 50040c497a819cd1229042ab3cb057d34a32cacc..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644
---- a/net/minecraft/util/ZeroBitStorage.java
-+++ b/net/minecraft/util/ZeroBitStorage.java
-@@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage {
- public BitStorage copy() {
- return this;
- }
-+
-+ // Paper start - block counting
-+ @Override
-+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() {
-+ final int size = this.size;
-+
-+ final short[] raw = new short[size];
-+ for (int i = 0; i < size; ++i) {
-+ raw[i] = (short)i;
-+ }
-+
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = it.unimi.dsi.fastutil.shorts.ShortArrayList.wrap(raw, size);
-+
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1);
-+ ret.put(0, coordinates);
-+ return ret;
-+ }
-+ // Paper end - block counting
- }
-diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
-index 766031d1482b0f49b196326b820d5ce9ae1c7c06..1f54752a4ea0788e73279cd99c7c35e3b5d9b6ce 100644
---- a/net/minecraft/world/entity/Entity.java
-+++ b/net/minecraft/world/entity/Entity.java
-@@ -176,7 +176,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
- import org.bukkit.plugin.PluginManager;
- // CraftBukkit end
-
--public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder {
-+public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity { // Paper - rewrite chunk system // Paper - optimise entity tracker
-
- // CraftBukkit start
- private static final int CURRENT_LEVEL = 2;
-@@ -187,7 +187,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-
- // Paper start - Share random for entities to make them more random
- public static RandomSource SHARED_RANDOM = new RandomRandomSource();
-- private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource {
-+ // Paper start - replace random
-+ private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom {
-+ public RandomRandomSource() {
-+ this(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed());
-+ }
-+
-+ public RandomRandomSource(long seed) {
-+ super(seed);
-+ }
-+
-+ // Paper end - replace random
- private boolean locked = false;
-
- @Override
-@@ -200,61 +210,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }
- }
-
-- @Override
-- public RandomSource fork() {
-- return new net.minecraft.world.level.levelgen.LegacyRandomSource(this.nextLong());
-- }
--
-- @Override
-- public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() {
-- return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong());
-- }
--
-- // these below are added to fix reobf issues that I don't wanna deal with right now
-- @Override
-- public int next(int bits) {
-- return super.next(bits);
-- }
--
-- @Override
-- public int nextInt(int origin, int bound) {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(origin, bound);
-- }
--
-- @Override
-- public long nextLong() {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextLong();
-- }
--
-- @Override
-- public int nextInt() {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt();
-- }
--
-- @Override
-- public int nextInt(int bound) {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextInt(bound);
-- }
--
-- @Override
-- public boolean nextBoolean() {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextBoolean();
-- }
--
-- @Override
-- public float nextFloat() {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextFloat();
-- }
--
-- @Override
-- public double nextDouble() {
-- return net.minecraft.world.level.levelgen.BitRandomSource.super.nextDouble();
-- }
--
-- @Override
-- public double nextGaussian() {
-- return super.nextGaussian();
-- }
-+ // Paper - replace random
- }
- // Paper end - Share random for entities to make them more random
- public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason
-@@ -462,6 +418,156 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return this.dimensions.makeBoundingBox(x, y, z);
- }
- // Paper end
-+ // Paper start - rewrite chunk system
-+ private final boolean isHardColliding = this.moonrise$isHardCollidingUncached();
-+ private net.minecraft.server.level.FullChunkStatus chunkStatus;
-+ private ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData;
-+ private int sectionX = Integer.MIN_VALUE;
-+ private int sectionY = Integer.MIN_VALUE;
-+ private int sectionZ = Integer.MIN_VALUE;
-+ private boolean updatingSectionStatus;
-+
-+ @Override
-+ public final boolean moonrise$isHardColliding() {
-+ return this.isHardColliding;
-+ }
-+
-+ @Override
-+ public final net.minecraft.server.level.FullChunkStatus moonrise$getChunkStatus() {
-+ return this.chunkStatus;
-+ }
-+
-+ @Override
-+ public final void moonrise$setChunkStatus(final net.minecraft.server.level.FullChunkStatus status) {
-+ this.chunkStatus = status;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData() {
-+ return this.chunkData;
-+ }
-+
-+ @Override
-+ public final void moonrise$setChunkData(final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) {
-+ this.chunkData = chunkData;
-+ }
-+
-+ @Override
-+ public final int moonrise$getSectionX() {
-+ return this.sectionX;
-+ }
-+
-+ @Override
-+ public final void moonrise$setSectionX(final int x) {
-+ this.sectionX = x;
-+ }
-+
-+ @Override
-+ public final int moonrise$getSectionY() {
-+ return this.sectionY;
-+ }
-+
-+ @Override
-+ public final void moonrise$setSectionY(final int y) {
-+ this.sectionY = y;
-+ }
-+
-+ @Override
-+ public final int moonrise$getSectionZ() {
-+ return this.sectionZ;
-+ }
-+
-+ @Override
-+ public final void moonrise$setSectionZ(final int z) {
-+ this.sectionZ = z;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$isUpdatingSectionStatus() {
-+ return this.updatingSectionStatus;
-+ }
-+
-+ @Override
-+ public final void moonrise$setUpdatingSectionStatus(final boolean to) {
-+ this.updatingSectionStatus = to;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$hasAnyPlayerPassengers() {
-+ if (this.passengers.isEmpty()) {
-+ return false;
-+ }
-+ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player);
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - optimise collisions
-+ private static float[] calculateStepHeights(final AABB box, final List<VoxelShape> voxels, final List<AABB> aabbs, final float stepHeight,
-+ final float collidedY) {
-+ final FloatArraySet ret = new FloatArraySet();
-+
-+ for (int i = 0, len = voxels.size(); i < len; ++i) {
-+ final VoxelShape shape = voxels.get(i);
-+
-+ final double[] yCoords = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$rootCoordinatesY();
-+ final double yOffset = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$offsetY();
-+
-+ for (final double yUnoffset : yCoords) {
-+ final double y = yUnoffset + yOffset;
-+
-+ final float step = (float)(y - box.minY);
-+
-+ if (step > stepHeight) {
-+ break;
-+ }
-+
-+ if (step < 0.0f || !(step != collidedY)) {
-+ continue;
-+ }
-+
-+ ret.add(step);
-+ }
-+ }
-+
-+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
-+ final AABB shape = aabbs.get(i);
-+
-+ final float step1 = (float)(shape.minY - box.minY);
-+ final float step2 = (float)(shape.maxY - box.minY);
-+
-+ if (!(step1 < 0.0f) && step1 != collidedY && !(step1 > stepHeight)) {
-+ ret.add(step1);
-+ }
-+
-+ if (!(step2 < 0.0f) && step2 != collidedY && !(step2 > stepHeight)) {
-+ ret.add(step2);
-+ }
-+ }
-+
-+ final float[] steps = ret.toFloatArray();
-+ FloatArrays.unstableSort(steps);
-+ return steps;
-+ }
-+ // Paper end - optimise collisions
-+ // Paper start - optimise entity tracker
-+ private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity;
-+
-+ @Override
-+ public final net.minecraft.server.level.ChunkMap.TrackedEntity moonrise$getTrackedEntity() {
-+ return this.trackedEntity;
-+ }
-+
-+ @Override
-+ public final void moonrise$setTrackedEntity(final net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity) {
-+ this.trackedEntity = trackedEntity;
-+ }
-+
-+ private static void collectIndirectPassengers(final List<Entity> into, final List<Entity> from) {
-+ for (final Entity passenger : from) {
-+ into.add(passenger);
-+ collectIndirectPassengers(into, ((Entity)(Object)passenger).passengers);
-+ }
-+ }
-+ // Paper end - optimise entity tracker
-
- public Entity(EntityType<?> type, Level world) {
- this.id = Entity.ENTITY_COUNTER.incrementAndGet();
-@@ -1387,41 +1493,76 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }
-
- private Vec3 collide(Vec3 movement) {
-- AABB axisalignedbb = this.getBoundingBox();
-- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement));
-- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list);
-- boolean flag = movement.x != vec3d1.x;
-- boolean flag1 = movement.y != vec3d1.y;
-- boolean flag2 = movement.z != vec3d1.z;
-- boolean flag3 = flag1 && movement.y < 0.0D;
--
-- if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) {
-- AABB axisalignedbb1 = flag3 ? axisalignedbb.move(0.0D, vec3d1.y, 0.0D) : axisalignedbb;
-- AABB axisalignedbb2 = axisalignedbb1.expandTowards(movement.x, (double) this.maxUpStep(), movement.z);
--
-- if (!flag3) {
-- axisalignedbb2 = axisalignedbb2.expandTowards(0.0D, -9.999999747378752E-6D, 0.0D);
-- }
-+ // Paper start - optimise collisions
-+ final boolean xZero = movement.x == 0.0;
-+ final boolean yZero = movement.y == 0.0;
-+ final boolean zZero = movement.z == 0.0;
-+ if (xZero & yZero & zZero) {
-+ return movement;
-+ }
-+
-+ final AABB currentBox = this.getBoundingBox();
-+
-+ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>();
-+ final List<AABB> potentialCollisionsBB = new ArrayList<>();
-
-- List<VoxelShape> list1 = Entity.collectColliders(this, this.level, list, axisalignedbb2);
-- float f = (float) vec3d1.y;
-- float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f);
-- float[] afloat1 = afloat;
-- int i = afloat.length;
-+ final AABB initialCollisionBox;
-+ if (xZero & zZero) {
-+ // note: xZero & zZero -> collision on x/z == 0 -> no step height calculation
-+ // this specifically optimises entities standing still
-+ initialCollisionBox = movement.y < 0.0 ?
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currentBox, movement.y) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currentBox, movement.y);
-+ } else {
-+ initialCollisionBox = currentBox.expandTowards(movement);
-+ }
-
-- for (int j = 0; j < i; ++j) {
-- float f1 = afloat1[j];
-- Vec3 vec3d2 = Entity.collideWithShapes(new Vec3(movement.x, (double) f1, movement.z), axisalignedbb1, list1);
-+ final List<AABB> entityAABBs = new ArrayList<>();
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions(
-+ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null
-+ );
-
-- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
-- double d0 = axisalignedbb.minY - axisalignedbb1.minY;
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
-+ this.level, (Entity)(Object)this, initialCollisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null
-+ );
-+ potentialCollisionsBB.addAll(entityAABBs);
-+ final Vec3 collided = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currentBox, potentialCollisionsVoxel, potentialCollisionsBB);
-
-- return vec3d2.add(0.0D, -d0, 0.0D);
-- }
-+ final boolean collidedX = collided.x != movement.x;
-+ final boolean collidedY = collided.y != movement.y;
-+ final boolean collidedZ = collided.z != movement.z;
-+
-+ final boolean collidedDownwards = collidedY && movement.y < 0.0;
-+
-+ final double stepHeight;
-+
-+ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) {
-+ return collided;
-+ }
-+
-+ final AABB collidedYBox = collidedDownwards ? currentBox.move(0.0, collided.y, 0.0) : currentBox;
-+ AABB stepRetrievalBox = collidedYBox.expandTowards(movement.x, stepHeight, movement.z);
-+ if (!collidedDownwards) {
-+ stepRetrievalBox = stepRetrievalBox.expandTowards(0.0, (double)-1.0E-5F, 0.0);
-+ }
-+
-+ final List<VoxelShape> stepVoxels = new ArrayList<>();
-+ final List<AABB> stepAABBs = entityAABBs;
-+
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
-+ this.level, (Entity)(Object)this, stepRetrievalBox, stepVoxels, stepAABBs,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, null
-+ );
-+
-+ for (final float step : calculateStepHeights(collidedYBox, stepVoxels, stepAABBs, (float)stepHeight, (float)collided.y)) {
-+ final Vec3 stepResult = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, (double)step, movement.z), collidedYBox, stepVoxels, stepAABBs);
-+ if (stepResult.horizontalDistanceSqr() > collided.horizontalDistanceSqr()) {
-+ return stepResult.add(0.0, collidedYBox.minY - currentBox.minY, 0.0);
- }
- }
-
-- return vec3d1;
-+ return collided;
-+ // Paper end - optimise collisions
- }
-
- private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) {
-@@ -2821,18 +2962,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }
-
- public boolean isInWall() {
-+ // Paper start - optimise collisions
- if (this.noPhysics) {
- return false;
-- } else {
-- float f = this.dimensions.width() * 0.8F;
-- AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
-+ }
-
-- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
-- BlockState iblockdata = this.level().getBlockState(blockposition);
-+ final double reducedWith = (double)(this.dimensions.width() * 0.8F);
-+ final AABB boundingBox = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith);
-+ final Level world = this.level;
-
-- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND);
-- });
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
-+ return false;
- }
-+
-+ final int minBlockX = Mth.floor(boundingBox.minX);
-+ final int minBlockY = Mth.floor(boundingBox.minY);
-+ final int minBlockZ = Mth.floor(boundingBox.minZ);
-+
-+ final int maxBlockX = Mth.floor(boundingBox.maxX);
-+ final int maxBlockY = Mth.floor(boundingBox.maxY);
-+ final int maxBlockZ = Mth.floor(boundingBox.maxZ);
-+
-+ final int minChunkX = minBlockX >> 4;
-+ final int minChunkY = minBlockY >> 4;
-+ final int minChunkZ = minBlockZ >> 4;
-+
-+ final int maxChunkX = maxBlockX >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-+
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world);
-+ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource();
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true).getSections();
-+
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
-+ continue;
-+ }
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
-+ continue;
-+ }
-+
-+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states;
-+
-+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0;
-+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15;
-+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0;
-+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15;
-+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0;
-+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15;
-+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ final int blockY = currY | (currChunkY << 4);
-+ mutablePos.setY(blockY);
-+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
-+ final int blockZ = currZ | (currChunkZ << 4);
-+ mutablePos.setZ(blockZ);
-+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
-+ final int blockX = currX | (currChunkX << 4);
-+ mutablePos.setX(blockX);
-+
-+ final BlockState blockState = blocks.get((currX) | (currZ << 4) | ((currY) << 8));
-+
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape()
-+ || !blockState.isSuffocating(world, mutablePos)) {
-+ continue;
-+ }
-+
-+ // Yes, it does not use the Entity context stuff.
-+ final VoxelShape collisionShape = blockState.getCollisionShape(world, mutablePos);
-+
-+ if (collisionShape.isEmpty()) {
-+ continue;
-+ }
-+
-+ final AABB toCollide = boundingBox.move(-(double)blockX, -(double)blockY, -(double)blockZ);
-+
-+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) {
-+ return true;
-+ }
-+ continue;
-+ }
-+
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
-+ return true;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ return false;
-+ // Paper end - optimise collisions
- }
-
- public InteractionResult interact(Player player, InteractionHand hand) {
-@@ -4310,14 +4543,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }
-
- public Iterable<Entity> getIndirectPassengers() {
-- // Paper start - Optimize indirect passenger iteration
-- if (this.passengers.isEmpty()) { return ImmutableList.of(); }
-- ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder();
-- for (Entity passenger : this.passengers) {
-- indirectPassengers.add(passenger);
-- indirectPassengers.addAll(passenger.getIndirectPassengers());
-+ // Paper start - optimise entity tracker
-+ final List<Entity> ret = new ArrayList<>();
-+
-+ if (this.passengers.isEmpty()) {
-+ return ret;
- }
-- return indirectPassengers.build();
-+
-+ collectIndirectPassengers(ret, this.passengers);
-+
-+ return ret;
-+ // Paper end - optimise entity tracker
- }
- private Iterable<Entity> getIndirectPassengers_old() {
- // Paper end - Optimize indirect passenger iteration
-@@ -4475,82 +4711,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return Mth.lerp(delta, this.yRotO, this.yRot);
- }
-
-- public boolean updateFluidHeightAndDoFluidPushing(TagKey<Fluid> tag, double speed) {
-+ // Paper start - optimise collisions
-+ public boolean updateFluidHeightAndDoFluidPushing(final TagKey<Fluid> fluid, final double flowScale) {
- if (this.touchingUnloadedChunk()) {
- return false;
-- } else {
-- AABB axisalignedbb = this.getBoundingBox().deflate(0.001D);
-- int i = Mth.floor(axisalignedbb.minX);
-- int j = Mth.ceil(axisalignedbb.maxX);
-- int k = Mth.floor(axisalignedbb.minY);
-- int l = Mth.ceil(axisalignedbb.maxY);
-- int i1 = Mth.floor(axisalignedbb.minZ);
-- int j1 = Mth.ceil(axisalignedbb.maxZ);
-- double d1 = 0.0D;
-- boolean flag = this.isPushedByFluid();
-- boolean flag1 = false;
-- Vec3 vec3d = Vec3.ZERO;
-- int k1 = 0;
-- BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
--
-- for (int l1 = i; l1 < j; ++l1) {
-- for (int i2 = k; i2 < l; ++i2) {
-- for (int j2 = i1; j2 < j1; ++j2) {
-- blockposition_mutableblockposition.set(l1, i2, j2);
-- FluidState fluid = this.level().getFluidState(blockposition_mutableblockposition);
--
-- if (fluid.is(tag)) {
-- double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition));
--
-- if (d2 >= axisalignedbb.minY) {
-- flag1 = true;
-- d1 = Math.max(d2 - axisalignedbb.minY, d1);
-- if (flag) {
-- Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition);
--
-- if (d1 < 0.4D) {
-- vec3d1 = vec3d1.scale(d1);
-- }
-+ }
-+
-+ final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3);
-+
-+ final Level world = this.level;
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world);
-+
-+ final int minBlockX = Mth.floor(boundingBox.minX);
-+ final int minBlockY = Math.max((minSection << 4), Mth.floor(boundingBox.minY));
-+ final int minBlockZ = Mth.floor(boundingBox.minZ);
-+
-+ // note: bounds are exclusive in Vanilla, so we subtract 1 - our loop expects bounds to be inclusive
-+ final int maxBlockX = Mth.ceil(boundingBox.maxX) - 1;
-+ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(world) << 4) | 15, Mth.ceil(boundingBox.maxY) - 1);
-+ final int maxBlockZ = Mth.ceil(boundingBox.maxZ) - 1;
-+
-+ final boolean isPushable = this.isPushedByFluid();
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+
-+ Vec3 pushVector = Vec3.ZERO;
-+ double totalPushes = 0.0;
-+ double maxHeightDiff = 0.0;
-+ boolean inFluid = false;
-+
-+ final int minChunkX = minBlockX >> 4;
-+ final int maxChunkX = maxBlockX >> 4;
-+
-+ final int minChunkY = minBlockY >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+
-+ final int minChunkZ = minBlockZ >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-+
-+ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource();
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false).getSections();
-+
-+ // bound y
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
-+ continue;
-+ }
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
-+ continue;
-+ }
-+
-+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states;
-+
-+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) : 0;
-+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) : 15;
-+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) : 0;
-+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) : 15;
-+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) : 0;
-+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) : 15;
-
-- vec3d = vec3d.add(vec3d1);
-- ++k1;
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
-+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
-+ final FluidState fluidState = blocks.get((currX) | (currZ << 4) | ((currY) << 8)).getFluidState();
-+
-+ if (fluidState.isEmpty() || !fluidState.is(fluid)) {
-+ continue;
- }
-- // CraftBukkit start - store last lava contact location
-- if (tag == FluidTags.LAVA) {
-- this.lastLavaContact = blockposition_mutableblockposition.immutable();
-+
-+ mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4));
-+
-+ final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos));
-+ final double diff = height - boundingBox.minY;
-+
-+ if (diff < 0.0) {
-+ continue;
-+ }
-+
-+ inFluid = true;
-+ maxHeightDiff = Math.max(maxHeightDiff, diff);
-+
-+ if (!isPushable) {
-+ continue;
-+ }
-+
-+ ++totalPushes;
-+
-+ final Vec3 flow = fluidState.getFlow(world, mutablePos);
-+
-+ if (diff < 0.4) {
-+ pushVector = pushVector.add(flow.scale(diff));
-+ } else {
-+ pushVector = pushVector.add(flow);
- }
-- // CraftBukkit end
- }
- }
- }
- }
- }
-+ }
-
-- if (vec3d.length() > 0.0D) {
-- if (k1 > 0) {
-- vec3d = vec3d.scale(1.0D / (double) k1);
-- }
-+ this.fluidHeight.put(fluid, maxHeightDiff);
-
-- if (!(this instanceof Player)) {
-- vec3d = vec3d.normalize();
-- }
-+ if (pushVector.lengthSqr() == 0.0) {
-+ return inFluid;
-+ }
-
-- Vec3 vec3d2 = this.getDeltaMovement();
-+ // note: totalPushes != 0 as pushVector != 0
-+ pushVector = pushVector.scale(1.0 / totalPushes);
-+ final Vec3 currMovement = this.getDeltaMovement();
-
-- vec3d = vec3d.scale(speed);
-- double d3 = 0.003D;
-+ if (!((Entity)(Object)this instanceof Player)) {
-+ pushVector = pushVector.normalize();
-+ }
-
-- if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) {
-- vec3d = vec3d.normalize().scale(0.0045000000000000005D);
-- }
-+ pushVector = pushVector.scale(flowScale);
-+ if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) {
-+ pushVector = pushVector.normalize().scale(0.0045000000000000005);
-+ }
-
-- this.setDeltaMovement(this.getDeltaMovement().add(vec3d));
-- }
-+ this.setDeltaMovement(currMovement.add(pushVector));
-
-- this.fluidHeight.put(tag, d1);
-- return flag1;
-- }
-+ // note: inFluid = true here as pushVector != 0
-+ return true;
- }
-+ // Paper end - optimise collisions
-
- public boolean touchingUnloadedChunk() {
- AABB axisalignedbb = this.getBoundingBox().inflate(1.0D);
-@@ -4702,6 +4992,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- this.setPosRaw(x, y, z, false);
- }
- public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
-+ // Paper start - rewrite chunk system
-+ if (this.updatingSectionStatus) {
-+ LOGGER.error(
-+ "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z)
-+ + " since it is processing a section status update", new Throwable()
-+ );
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
- if (!checkPosition(this, x, y, z)) {
- return;
- }
-@@ -4831,6 +5130,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-
- @Override
- public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
-+ // Paper start - rewrite chunk system
-+ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) {
-+ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable());
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
- CraftEventFactory.callEntityRemoveEvent(this, cause);
- // CraftBukkit end
- final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
-@@ -4842,7 +5147,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- this.stopRiding();
- }
-
-- this.getPassengers().forEach(Entity::stopRiding);
-+ if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
- this.levelCallback.onRemove(entity_removalreason);
- this.onRemoval(entity_removalreason);
- // Paper start - Folia schedulers
-@@ -4874,7 +5179,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
-
- @Override
- public boolean shouldBeSaved() {
-- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
-+ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
- }
-
- @Override
-diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 96bc0ba60195e5e666d47b3a0b943b733986d96a..5930a430983061afddf20e3208ff2462ca1b78cd 100644
---- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-+++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -38,12 +38,137 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
- import net.minecraft.world.level.chunk.storage.SectionStorage;
- import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
-
--public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
-+public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system
- public static final int MAX_VILLAGE_DISTANCE = 6;
- public static final int VILLAGE_SECTION_SIZE = 1;
- private final PoiManager.DistanceTracker distanceTracker;
- private final LongSet loadedChunks = new LongOpenHashSet();
-
-+ // Paper start - rewrite chunk system
-+ private final net.minecraft.server.level.ServerLevel world;
-+
-+ // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes
-+ // to support poi unloading
-+ private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D();
-+
-+ private static final int POI_DATA_SOURCE = 7;
-+
-+ private static int convertBetweenLevels(final int level) {
-+ return POI_DATA_SOURCE - level;
-+ }
-+
-+ private void updateDistanceTracking(long section) {
-+ if (this.isVillageCenter(section)) {
-+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
-+ } else {
-+ this.villageDistanceTracker.removeSource(section);
-+ }
-+ }
-+
-+ @Override
-+ public Optional<PoiSection> get(final long pos) {
-+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
-+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
-+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
-+
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
-+
-+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY);
-+ }
-+
-+ @Override
-+ public Optional<PoiSection> getOrLoad(final long pos) {
-+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
-+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
-+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
-+
-+ 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;
-+
-+ if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
-+ if (ret != null) {
-+ return ret.getSectionForVanilla(chunkY);
-+ } else {
-+ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY);
-+ }
-+ }
-+ // retain vanilla behavior: do not load section if out of bounds!
-+ return Optional.empty();
-+ }
-+
-+ @Override
-+ protected PoiSection getOrCreate(final long pos) {
-+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
-+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
-+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
-+
-+ 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);
-+ if (ret != null) {
-+ return ret.getOrCreateSection(chunkY);
-+ } else {
-+ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY);
-+ }
-+ }
-+
-+ @Override
-+ public final net.minecraft.server.level.ServerLevel moonrise$getWorld() {
-+ return this.world;
-+ }
-+
-+ @Override
-+ public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system
-+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate);
-+
-+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world);
-+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world);
-+
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main");
-+ for (int sectionY = minY; sectionY <= maxY; ++sectionY) {
-+ final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ);
-+ this.updateDistanceTracking(sectionPos);
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) {
-+ final int chunkX = poiChunk.chunkX;
-+ final int chunkZ = poiChunk.chunkZ;
-+
-+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world);
-+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world);
-+
-+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main");
-+ for (int sectionY = minY; sectionY <= maxY; ++sectionY) {
-+ final PoiSection section = poiChunk.getSection(sectionY);
-+ if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) {
-+ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ));
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+
-+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk);
-+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk);
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ for (int section = minY; section <= maxY; ++section) {
-+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]);
-+ }
-+ }
-+ // Paper end - rewrite chunk system
-+
- public PoiManager(
- RegionStorageInfo storageKey,
- Path directory,
-@@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
- world
- );
- this.distanceTracker = new PoiManager.DistanceTracker();
-+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
- }
-
- public void add(BlockPos pos, Holder<PoiType> type) {
-@@ -197,8 +323,10 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
- }
-
- public int sectionsToVillage(SectionPos pos) {
-- this.distanceTracker.runAllUpdates();
-- return this.distanceTracker.getLevel(pos.asLong());
-+ // Paper start - rewrite chunk system
-+ this.villageDistanceTracker.propagateUpdates();
-+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos)));
-+ // Paper end - rewrite chunk system
- }
-
- boolean isVillageCenter(long pos) {
-@@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
-
- @Override
- public void tick(BooleanSupplier shouldKeepTicking) {
-- super.tick(shouldKeepTicking);
-- this.distanceTracker.runAllUpdates();
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
- }
-
- @Override
-- protected void setDirty(long pos) {
-- super.setDirty(pos);
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ public void setDirty(long pos) { // Paper - public
-+ // Paper start - rewrite chunk system
-+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
-+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
-+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
-+ if (chunk != null) {
-+ chunk.setDirty(true);
-+ }
-+ this.updateDistanceTracking(pos);
-+ // Paper end - rewrite chunk system
- }
-
- @Override
- protected void onSectionLoad(long pos) {
-- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
-+ this.updateDistanceTracking(pos); // Paper - rewrite chunk system
- }
-
- public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
-@@ -263,7 +398,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
- .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong())))
- .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false))
- .map(pair -> pair.getFirst().chunk())
-- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong()))
-+ // Paper - rewrite chunk system
- .forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
- }
-
-diff --git a/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-index b9e0bc8f1e948614d986335de1f3d2df199eea81..712cbfc100e8aaf612d1d651dae64f57f892a768 100644
---- a/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-+++ b/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-@@ -23,13 +23,27 @@ import net.minecraft.core.SectionPos;
- import net.minecraft.util.VisibleForDebug;
- import org.slf4j.Logger;
-
--public class PoiSection {
-+public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system
- private static final Logger LOGGER = LogUtils.getLogger();
- private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
- private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
- private final Runnable setDirty;
- private boolean isValid;
-
-+ // Paper start - rewrite chunk system
-+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);
-+
-+ @Override
-+ public final boolean moonrise$isEmpty() {
-+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty();
-+ }
-+
-+ @Override
-+ public final Optional<PoiSection> moonrise$asOptional() {
-+ return this.noAllocOptional;
-+ }
-+ // Paper end - rewrite chunk system
-+
- public PoiSection(Runnable updateListener) {
- this(updateListener, true, ImmutableList.of());
- }
-diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java
-index 63f02cdc67d9e88cc6998d0ae9d139c83e85b447..70b8023c3badc745f342d5b0ab54699e3923826a 100644
---- a/net/minecraft/world/entity/decoration/ArmorStand.java
-+++ b/net/minecraft/world/entity/decoration/ArmorStand.java
-@@ -364,7 +364,7 @@ public class ArmorStand extends LivingEntity {
- @Override
- protected void pushEntities() {
- if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
-- List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
-+ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions
- Iterator iterator = list.iterator();
-
- while (iterator.hasNext()) {
-diff --git a/net/minecraft/world/level/ClipContext.java b/net/minecraft/world/level/ClipContext.java
-index 3fa2964b979053ecbefc946c7fe76828de86d8f1..28bf0518f7d17099d7e4990defbeda6757b4477c 100644
---- a/net/minecraft/world/level/ClipContext.java
-+++ b/net/minecraft/world/level/ClipContext.java
-@@ -18,7 +18,7 @@ public class ClipContext {
- private final Vec3 from;
- private final Vec3 to;
- private final ClipContext.Block block;
-- private final ClipContext.Fluid fluid;
-+ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public
- private final CollisionContext collisionContext;
-
- public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
-diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java
-index e185a33b5b1f8e8e0a0e666b24ba3e9186a8a7ff..5d7a6e4b73f032db356e7ec369b150013e940ee6 100644
---- a/net/minecraft/world/level/EntityGetter.java
-+++ b/net/minecraft/world/level/EntityGetter.java
-@@ -15,7 +15,7 @@ import net.minecraft.world.phys.shapes.BooleanOp;
- import net.minecraft.world.phys.shapes.Shapes;
- import net.minecraft.world.phys.shapes.VoxelShape;
-
--public interface EntityGetter {
-+public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
- List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
-
- <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate);
-@@ -30,21 +30,44 @@ public interface EntityGetter {
- return this.getEntities(except, box, EntitySelector.NO_SPECTATORS);
- }
-
-- default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
-- if (shape.isEmpty()) {
-- return true;
-- } else {
-- for (Entity entity : this.getEntities(except, shape.bounds())) {
-- if (!entity.isRemoved()
-- && entity.blocksBuilding
-- && (except == null || !entity.isPassengerOfSameVehicle(except))
-- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) {
-- return false;
-+ // Paper start - rewrite chunk system
-+ @Override
-+ default List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
-+ return this.getEntities(entity, box, predicate);
-+ }
-+ // Paper end - rewrite chunk system
-+
-+ // Paper start - optimise collisions
-+ default boolean isUnobstructed(@Nullable Entity entity, VoxelShape voxel) {
-+ if (voxel.isEmpty()) {
-+ return false;
-+ }
-+
-+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
-+ final List<Entity> entities = this.getEntities(
-+ entity,
-+ singleAABB == null ? voxel.bounds() : singleAABB.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)
-+ );
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) {
-+ continue;
-+ }
-+
-+ if (singleAABB == null) {
-+ final AABB entityBB = otherEntity.getBoundingBox();
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(entityBB) || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) {
-+ continue;
- }
- }
-
-- return true;
-+ return false;
- }
-+
-+ return true;
-+ // Paper end - optimise collisions
- }
-
- default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) {
-@@ -52,23 +75,41 @@ public interface EntityGetter {
- }
-
- default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
-- if (box.getSize() < 1.0E-7) {
-- return List.of();
-+ // Paper start - optimise collisions
-+ // first behavior change is to correctly check for empty AABB
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) {
-+ // reduce indirection by always returning type with same class
-+ return new java.util.ArrayList<>();
-+ }
-+
-+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
-+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
-+ // specifically with boat collisions.
-+ box = box.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON);
-+
-+ final List<Entity> entities;
-+ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+ entities = this.getEntities(entity, box, null);
- } else {
-- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith);
-- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate);
-- if (list.isEmpty()) {
-- return List.of();
-- } else {
-- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
--
-- for (Entity entity2 : list) {
-- builder.add(Shapes.create(entity2.getBoundingBox()));
-- }
-+ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null);
-+ }
-
-- return builder.build();
-+ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size()));
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator()) {
-+ continue;
-+ }
-+
-+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
-+ ret.add(Shapes.create(otherEntity.getBoundingBox()));
- }
- }
-+
-+ return ret;
-+ // Paper end - optimise collisions
- }
-
- // Paper start - Affects Spawning API
-diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
-index 2a078293332efe4369f314ab021dfa16f63f7f3f..f477c5817f022ce7c4ad25e9b827401434bcfff1 100644
---- a/net/minecraft/world/level/Level.java
-+++ b/net/minecraft/world/level/Level.java
-@@ -84,6 +84,7 @@ import net.minecraft.world.level.storage.LevelData;
- import net.minecraft.world.level.storage.WritableLevelData;
- import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.Vec3;
-+import net.minecraft.world.phys.shapes.VoxelShape;
- import net.minecraft.world.scores.Scoreboard;
-
- // CraftBukkit start
-@@ -105,7 +106,7 @@ import org.bukkit.entity.SpawnCategory;
- import org.bukkit.event.block.BlockPhysicsEvent;
- // CraftBukkit end
-
--public abstract class Level implements LevelAccessor, AutoCloseable {
-+public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system // Paper - optimise collisions
-
- public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
- public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
-@@ -131,7 +132,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- public float rainLevel;
- protected float oThunderLevel;
- public float thunderLevel;
-- public final RandomSource random = RandomSource.create();
-+ public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random
- /** @deprecated */
- @Deprecated
- private final RandomSource threadSafeRandom = RandomSource.createThreadSafe();
-@@ -207,7 +208,639 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
-
- public abstract ResourceKey<LevelStem> getTypeKey();
-
-+ // Paper start - rewrite chunk system
-+ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup;
-+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData> chunkData = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() {
-+ return this.entityLookup;
-+ }
-+
-+ @Override
-+ public final void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) {
-+ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) {
-+ throw new IllegalStateException("Entity lookup already initialised");
-+ }
-+ this.entityLookup = entityLookup;
-+ }
-+
-+ @Override
-+ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) {
-+ Profiler.get().incrementCounter("getEntities");
-+ final List<T> ret = new java.util.ArrayList<>();
-+
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
-+ Profiler.get().incrementCounter("getEntities");
-+ final List<Entity> ret = new java.util.ArrayList<>();
-+
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
-+ return (LevelChunk)this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.FULL, false);
-+ }
-+
-+ @Override
-+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
-+ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false);
-+ }
-+
-+ @Override
-+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) {
-+ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false);
-+ }
-+
-+ @Override
-+ public void moonrise$midTickTasks() {
-+ // no-op on ClientLevel
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final long chunkKey) {
-+ return this.chunkData.get(chunkKey);
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$getChunkData(final int chunkX, final int chunkZ) {
-+ return this.chunkData.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$requestChunkData(final long chunkKey) {
-+ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData valueInMap) -> {
-+ if (valueInMap == null) {
-+ final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData ret = new ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData();
-+ ret.increaseRef();
-+ return ret;
-+ }
-+
-+ valueInMap.increaseRef();
-+ return valueInMap;
-+ });
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData moonrise$releaseChunkData(final long chunkKey) {
-+ return this.chunkData.compute(chunkKey, (final long keyInMap, final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData) -> {
-+ return chunkData.decreaseRef() == 0 ? null : chunkData;
-+ });
-+ }
-+
-+ @Override
-+ public boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) {
-+ final ChunkSource chunkSource = this.getChunkSource();
-+
-+ for (int currZ = fromZ; currZ <= toZ; ++currZ) {
-+ for (int currX = fromX; currX <= toX; ++currX) {
-+ if (!chunkSource.hasChunk(currX, currZ)) {
-+ return false;
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean hasChunksAt(final int minBlockX, final int minBlockZ, final int maxBlockX, final int maxBlockZ) {
-+ return this.moonrise$areChunksLoaded(
-+ minBlockX >> 4, minBlockZ >> 4, maxBlockX >> 4, maxBlockZ >> 4
-+ );
-+ }
-+
-+ /**
-+ * @reason Turn all getChunk(x, z, status) calls into virtual invokes, instead of interface invokes:
-+ * 1. The interface invoke is expensive
-+ * 2. The method makes other interface invokes (again, expensive)
-+ * Instead, we just directly call getChunk(x, z, status, true) which avoids the interface invokes entirely.
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public ChunkAccess getChunk(final int x, final int z, final ChunkStatus status) {
-+ return ((Level)(Object)this).getChunk(x, z, status, true);
-+ }
-+
-+ @Override
-+ public BlockPos getHeightmapPos(Heightmap.Types types, BlockPos blockPos) {
-+ return new BlockPos(blockPos.getX(), this.getHeight(types, blockPos.getX(), blockPos.getZ()), blockPos.getZ());
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - optimise collisions
-+ /**
-+ * Route to faster lookup.
-+ * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public boolean isUnobstructed(final Entity entity) {
-+ final AABB boundingBox = entity.getBoundingBox();
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
-+ return false;
-+ }
-+
-+ final List<Entity> entities = this.getEntities(
-+ entity,
-+ boundingBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON),
-+ null
-+ );
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) {
-+ continue;
-+ }
-+
-+ return false;
-+ }
-+
-+ return true;
-+ }
-+
-+
-+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) {
-+ final Vec3 to = clipContext.getTo();
-+ final Vec3 from = clipContext.getFrom();
-+
-+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getApproximateNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
-+ }
-+
-+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
-+
-+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level,
-+ final ClipContext clipContext) {
-+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
-+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
-+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
-+
-+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
-+ return miss(clipContext);
-+ }
-+
-+ final double toXAdj = to.x - adjX;
-+ final double toYAdj = to.y - adjY;
-+ final double toZAdj = to.z - adjZ;
-+ final double fromXAdj = from.x + adjX;
-+ final double fromYAdj = from.y + adjY;
-+ final double fromZAdj = from.z + adjZ;
-+
-+ int currX = Mth.floor(fromXAdj);
-+ int currY = Mth.floor(fromYAdj);
-+ int currZ = Mth.floor(fromZAdj);
-+
-+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos();
-+
-+ final double diffX = toXAdj - fromXAdj;
-+ final double diffY = toYAdj - fromYAdj;
-+ final double diffZ = toZAdj - fromZAdj;
-+
-+ final double dxDouble = Math.signum(diffX);
-+ final double dyDouble = Math.signum(diffY);
-+ final double dzDouble = Math.signum(diffZ);
-+
-+ final int dx = (int)dxDouble;
-+ final int dy = (int)dyDouble;
-+ final int dz = (int)dzDouble;
-+
-+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
-+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
-+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
-+
-+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
-+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
-+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
-+
-+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null;
-+ net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> lastSection = null;
-+ int lastChunkX = Integer.MIN_VALUE;
-+ int lastChunkY = Integer.MIN_VALUE;
-+ int lastChunkZ = Integer.MIN_VALUE;
-+
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
-+
-+ for (;;) {
-+ currPos.set(currX, currY, currZ);
-+
-+ final int newChunkX = currX >> 4;
-+ final int newChunkY = currY >> 4;
-+ final int newChunkZ = currZ >> 4;
-+
-+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
-+ final int chunkYDiff = newChunkY ^ lastChunkY;
-+
-+ if ((chunkDiff | chunkYDiff) != 0) {
-+ if (chunkDiff != 0) {
-+ lastChunk = level.getChunk(newChunkX, newChunkZ).getSections();
-+ }
-+ final int sectionY = newChunkY - minSection;
-+ lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null;
-+
-+ lastChunkX = newChunkX;
-+ lastChunkY = newChunkY;
-+ lastChunkZ = newChunkZ;
-+ }
-+
-+ final BlockState blockState;
-+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) {
-+ final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos);
-+
-+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState);
-+
-+ final VoxelShape fluidCollision;
-+ final FluidState fluidState;
-+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) {
-+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos);
-+
-+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos);
-+
-+ if (fluidHit != null) {
-+ if (blockHit == null) {
-+ return fluidHit;
-+ }
-+
-+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit;
-+ }
-+ }
-+
-+ if (blockHit != null) {
-+ return blockHit;
-+ }
-+ } // else: usually fall here
-+
-+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
-+ return miss(clipContext);
-+ }
-+
-+ // inc the smallest normalized coordinate
-+
-+ if (normalizedCurrX < normalizedCurrY) {
-+ if (normalizedCurrX < normalizedCurrZ) {
-+ currX += dx;
-+ normalizedCurrX += normalizedDiffX;
-+ } else {
-+ // x < y && x >= z <--> z < y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ } else if (normalizedCurrY < normalizedCurrZ) {
-+ // y <= x && y < z
-+ currY += dy;
-+ normalizedCurrY += normalizedDiffY;
-+ } else {
-+ // y <= x && z <= y <--> z <= y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ }
-+ }
-+
-+ /**
-+ * @reason Route to optimized call
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
-+ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks
-+ return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext);
-+ }
-+
-+ /**
-+ * @reason Route to faster logic
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY,
-+ (final BlockState state, final BlockPos pos) -> {
-+ return state.isSuffocating((Level)(Object)Level.this, pos);
-+ }
-+ );
-+ }
-+
-+ private static VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) {
-+ return net.minecraft.world.phys.shapes.Shapes.create(
-+ aabb.minX - x,
-+ aabb.minY - y,
-+ aabb.minZ - z,
-+
-+ aabb.maxX + x,
-+ aabb.maxY + y,
-+ aabb.maxZ + z
-+ );
-+ }
-+
-+ /**
-+ * @reason Use optimised OR operator join strategy, avoid streams
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public java.util.Optional<net.minecraft.world.phys.Vec3> findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition,
-+ final double rangeX, final double rangeY, final double rangeZ) {
-+ if (boundsShape.isEmpty()) {
-+ return java.util.Optional.empty();
-+ }
-+
-+ final double expandByX = rangeX * 0.5;
-+ final double expandByY = rangeY * 0.5;
-+ final double expandByZ = rangeZ * 0.5;
-+
-+ // note: it is useless to look at shapes outside of range / 2.0
-+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ);
-+
-+ final List<AABB> aabbs = new java.util.ArrayList<>();
-+ final List<VoxelShape> voxels = new java.util.ArrayList<>();
-+
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
-+ (Level)(Object)this, entity, collectionVolume, voxels, aabbs,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
-+ null
-+ );
-+
-+ final WorldBorder worldBorder = this.getWorldBorder();
-+ if (worldBorder != null) {
-+ aabbs.removeIf((final AABB aabb) -> {
-+ return !worldBorder.isWithinBounds(aabb);
-+ });
-+ voxels.removeIf((final VoxelShape shape) -> {
-+ return !worldBorder.isWithinBounds(shape.bounds());
-+ });
-+ }
-+
-+ // push voxels into aabbs
-+ for (int i = 0, len = voxels.size(); i < len; ++i) {
-+ aabbs.addAll(voxels.get(i).toAabbs());
-+ }
-+
-+ // expand AABBs
-+ final VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ);
-+ final VoxelShape[] rest = new VoxelShape[Math.max(0, aabbs.size() - 1)];
-+
-+ for (int i = 1, len = aabbs.size(); i < len; ++i) {
-+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ);
-+ }
-+
-+ // use optimized implementation of ORing the shapes together
-+ final VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest);
-+
-+ // find free space
-+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs()
-+ final VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST);
-+
-+ return freeSpace.closestPointTo(fromPosition);
-+ }
-+
-+ /**
-+ * @reason Route to faster logic
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((Level)(Object)this);
-+
-+ final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1;
-+
-+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1);
-+ final int maxBlockY = Math.min((ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection((Level)(Object)this) << 4) + 16, Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1);
-+
-+ final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1;
-+
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity);
-+ BlockPos selected = null;
-+ double selectedDistance = Double.MAX_VALUE;
-+ final Vec3 entityPos = entity.position();
-+
-+ // special cases:
-+ if (minBlockY > maxBlockY) {
-+ // no point in checking
-+ return java.util.Optional.empty();
-+ }
-+
-+ final int minChunkX = minBlockX >> 4;
-+ final int maxChunkX = maxBlockX >> 4;
-+
-+ final int minChunkY = minBlockY >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+
-+ final int minChunkZ = minBlockZ >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-+
-+ final ChunkSource chunkSource = this.getChunkSource();
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, false);
-+
-+ if (chunk == null) {
-+ continue;
-+ }
-+
-+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
-+
-+ // bound y
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
-+ continue;
-+ }
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
-+ continue;
-+ }
-+
-+ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks();
-+ final int sectionAdjust = !hasSpecial ? 1 : 0;
-+
-+ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states;
-+
-+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
-+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
-+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0;
-+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15;
-+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0;
-+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15;
-+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ final int blockY = currY | (currChunkY << 4);
-+ mutablePos.setY(blockY);
-+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
-+ final int blockZ = currZ | (currChunkZ << 4);
-+ mutablePos.setZ(blockZ);
-+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
-+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
-+ final int blockX = currX | (currChunkX << 4);
-+ mutablePos.setX(blockX);
-+
-+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
-+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
-+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0;
-+ if (edgeCount == 3) {
-+ continue;
-+ }
-+
-+ final double distance = mutablePos.distToCenterSqr(entityPos);
-+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) {
-+ continue;
-+ }
-+
-+ final BlockState blockData = blocks.get(localBlockIndex);
-+
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) {
-+ continue;
-+ }
-+
-+ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape();
-+
-+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
-+ if (blockCollision == null) {
-+ blockCollision = blockData.getCollisionShape((Level)(Object)this, mutablePos, collisionShape);
-+
-+ if (blockCollision.isEmpty()) {
-+ continue;
-+ }
-+ }
-+
-+ // avoid VoxelShape#move by shifting the entity collision shape instead
-+ final AABB shiftedAABB = aabb.move(-(double)blockX, -(double)blockY, -(double)blockZ);
-+
-+ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) {
-+ continue;
-+ }
-+
-+ selected = mutablePos.immutable();
-+ selectedDistance = distance;
-+ continue;
-+ }
-+
-+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
-+ continue;
-+ }
-+
-+ selected = mutablePos.immutable();
-+ selectedDistance = distance;
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ return java.util.Optional.ofNullable(selected);
-+ }
-+ // Paper end - optimise collisions
-+ // Paper start - getblock optimisations - cache world height/sections
-+ private final int minY;
-+ private final int height;
-+ private final int maxY;
-+ private final int minSectionY;
-+ private final int maxSectionY;
-+ private final int sectionsCount;
-+
-+ @Override
-+ public int getMinY() {
-+ return this.minY;
-+ }
-+
-+ @Override
-+ public int getHeight() {
-+ return this.height;
-+ }
-+
-+ @Override
-+ public int getMaxY() {
-+ return this.maxY;
-+ }
-+
-+ @Override
-+ public int getSectionsCount() {
-+ return this.sectionsCount;
-+ }
-+
-+ @Override
-+ public int getMinSectionY() {
-+ return this.minSectionY;
-+ }
-+
-+ @Override
-+ public int getMaxSectionY() {
-+ return this.maxSectionY;
-+ }
-+
-+ @Override
-+ public boolean isInsideBuildHeight(final int blockY) {
-+ return blockY >= this.minY && blockY <= this.maxY;
-+ }
-+
-+ @Override
-+ public boolean isOutsideBuildHeight(final BlockPos pos) {
-+ return this.isOutsideBuildHeight(pos.getY());
-+ }
-+
-+ @Override
-+ public boolean isOutsideBuildHeight(final int blockY) {
-+ return blockY < this.minY || blockY > this.maxY;
-+ }
-+
-+ @Override
-+ public int getSectionIndex(final int blockY) {
-+ return (blockY >> 4) - this.minSectionY;
-+ }
-+
-+ @Override
-+ public int getSectionIndexFromSectionY(final int sectionY) {
-+ return sectionY - this.minSectionY;
-+ }
-+
-+ @Override
-+ public int getSectionYFromSectionIndex(final int sectionIdx) {
-+ return sectionIdx + this.minSectionY;
-+ }
-+ // Paper end - getblock optimisations - cache world height/sections
-+ // Paper start - optimise random ticking
-+ @Override
-+ public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z);
-+
-+ /**
-+ * @reason Make getChunk and getUncachedNoiseBiome virtual calls instead of interface calls
-+ * by implementing the superclass method in this class.
-+ * @author Spottedleaf
-+ */
-+ @Override
-+ public Holder<Biome> getNoiseBiome(final int x, final int y, final int z) {
-+ final ChunkAccess chunk = this.getChunk(x >> 2, z >> 2, ChunkStatus.BIOMES, false);
-+
-+ return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z);
-+ }
-+ // Paper end - optimise random ticking
-+
- protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray
-+ // Paper start - getblock optimisations - cache world height/sections
-+ final DimensionType dimType = holder.value();
-+ this.minY = dimType.minY();
-+ this.height = dimType.height();
-+ this.maxY = this.minY + this.height - 1;
-+ this.minSectionY = this.minY >> 4;
-+ this.maxSectionY = this.maxY >> 4;
-+ this.sectionsCount = this.maxSectionY - this.minSectionY + 1;
-+ // Paper end - getblock optimisations - cache world height/sections
- this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
- this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
- this.generator = gen;
-@@ -288,6 +921,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
- this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
- this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
-+ this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system
- }
-
- // Paper start - Cancel hit for vanished players
-@@ -557,7 +1191,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
- }
-
-- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
-+ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full
- this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
- }
-
-@@ -820,6 +1454,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
- boolean flag = this.tickRateManager().runsNormally();
-
-+ int tickedEntities = 0; // Paper - rewrite chunk system
-+
- int tilesThisCycle = 0;
- var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
- toRemove.add(null); // Paper - Fix MC-117075
-@@ -835,6 +1471,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- // Spigot end
- } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
- tickingblockentity.tick();
-+ // Paper start - rewrite chunk system
-+ if ((++tickedEntities & 7) == 0) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks();
-+ }
-+ // Paper end - rewrite chunk system
- }
- }
- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
-@@ -855,12 +1496,20 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
- // Paper end - Prevent block entity and entity crashes
- }
-+ this.moonrise$midTickTasks(); // Paper - rewrite chunk system
- }
- // Paper start - Option to prevent armor stands from doing entity lookups
- @Override
- public boolean noCollision(@Nullable Entity entity, AABB box) {
- if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
-- return LevelAccessor.super.noCollision(entity, box);
-+ // Paper start - optimise collisions
-+ final int flags = entity == null ? (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, flags, null)) {
-+ return false;
-+ }
-+
-+ return !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions((Level)(Object)this, entity, box, null, flags, null);
-+ // Paper end - optimise collisions
- }
- // Paper end - Option to prevent armor stands from doing entity lookups
-
-@@ -912,7 +1561,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- }
- // 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 && !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) {
-@@ -1004,23 +1653,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- Profiler.get().incrementCounter("getEntities");
- List<Entity> list = Lists.newArrayList();
-
-- this.getEntities().get(box, (entity1) -> {
-- if (entity1 != except && predicate.test(entity1)) {
-- list.add(entity1);
-- }
--
-- });
-- Iterator iterator = this.dragonParts().iterator();
-+ // Paper start - rewrite chunk system
-+ final List<Entity> ret = new java.util.ArrayList<>();
-
-- while (iterator.hasNext()) {
-- EnderDragonPart entitycomplexpart = (EnderDragonPart) iterator.next();
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
-
-- if (entitycomplexpart != except && entitycomplexpart.parentMob != except && predicate.test(entitycomplexpart) && box.intersects(entitycomplexpart.getBoundingBox())) {
-- list.add(entitycomplexpart);
-- }
-- }
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, except, box, predicate, ret);
-
-- return list;
-+ return ret;
-+ // Paper end - rewrite chunk system
- }
-
- @Override
-@@ -1035,36 +1676,94 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
- }
-
-- public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
-+ // Paper start - rewrite chunk system
-+ public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
-+ final AABB boundingBox, final Predicate<? super T> predicate,
-+ final List<? super T> into, final int maxCount) {
- Profiler.get().incrementCounter("getEntities");
-- this.getEntities().get(filter, box, (entity) -> {
-- if (predicate.test(entity)) {
-- result.add(entity);
-- if (result.size() >= limit) {
-- return AbortableIterationConsumer.Continuation.ABORT;
-- }
-+
-+ if (entityTypeTest instanceof net.minecraft.world.entity.EntityType<T> byType) {
-+ if (maxCount != Integer.MAX_VALUE) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ } else {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
- }
-+ }
-
-- if (entity instanceof EnderDragon entityenderdragon) {
-- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities();
-- int j = aentitycomplexpart.length;
-+ if (entityTypeTest == null) {
-+ if (maxCount != Integer.MAX_VALUE) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ } else {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ }
-+ }
-
-- for (int k = 0; k < j; ++k) {
-- EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
-- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
-+ final Class<? extends Entity> base = entityTypeTest.getBaseClass();
-
-- if (t0 != null && predicate.test(t0)) {
-- result.add(t0);
-- if (result.size() >= limit) {
-- return AbortableIterationConsumer.Continuation.ABORT;
-- }
-- }
-+ final Predicate<? super T> modifiedPredicate;
-+ if (predicate == null) {
-+ modifiedPredicate = (final T obj) -> {
-+ return entityTypeTest.tryCast(obj) != null;
-+ };
-+ } else {
-+ modifiedPredicate = (final Entity obj) -> {
-+ final T casted = entityTypeTest.tryCast(obj);
-+ if (casted == null) {
-+ return false;
- }
-+
-+ return predicate.test(casted);
-+ };
-+ }
-+
-+ if (base == null || base == Entity.class) {
-+ if (maxCount != Integer.MAX_VALUE) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ } else {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ }
-+ } else {
-+ if (maxCount != Integer.MAX_VALUE) {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
-+ } else {
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate);
-+ ca.spottedleaf.moonrise.common.PlatformHooks.get().addToGetEntities((Level)(Object)this, entityTypeTest, boundingBox, predicate, into, maxCount);
-+ return;
- }
-+ }
-+ }
-
-- return AbortableIterationConsumer.Continuation.CONTINUE;
-- });
-+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
-+ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ);
-+ if (slices == null) {
-+ return new org.bukkit.entity.Entity[0];
-+ }
-+
-+ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
-+ for (Entity entity : slices.getAllEntities()) {
-+ org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
-+ if (bukkit != null && bukkit.isValid()) {
-+ ret.add(bukkit);
-+ }
-+ }
-+
-+ return ret.toArray(new org.bukkit.entity.Entity[0]);
- }
-+ // Paper end - rewrite chunk system
-
- @Nullable
- public abstract Entity getEntity(int id);
-diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java
-index 5eb8982678110fabb82a93c5ec67c666b7fde017..ade435de0af4ee3566fa4a490df53cddd2f6531c 100644
---- a/net/minecraft/world/level/LevelReader.java
-+++ b/net/minecraft/world/level/LevelReader.java
-@@ -22,7 +22,18 @@ import net.minecraft.world.level.dimension.DimensionType;
- import net.minecraft.world.level.levelgen.Heightmap;
- import net.minecraft.world.phys.AABB;
-
--public interface LevelReader extends BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource {
-+public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource { // Paper - rewrite chunk system
-+
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public default ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
-+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
-+ throw new IllegalArgumentException("Status: " + status.toString());
-+ }
-+ return ((LevelReader)this).getChunk(chunkX, chunkZ, status, true);
-+ }
-+ // Paper end - rewrite chunk system
-+
- @Nullable
- ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
-
-diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java
-index b8ffe547ad29645b65c3df8bd6ccb7c20985711d..685ccfb73bf7125585ef90b6a0f51b2f81daa428 100644
---- a/net/minecraft/world/level/ServerExplosion.java
-+++ b/net/minecraft/world/level/ServerExplosion.java
-@@ -64,6 +64,249 @@ public class ServerExplosion implements Explosion {
- public float yield;
- // CraftBukkit end
- public boolean excludeSourceFromDamage = true; // Paper - Allow explosions to damage source
-+ // Paper start - collisions optimisations
-+ private static final double[] CACHED_RAYS;
-+ static {
-+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList();
-+
-+ for (int x = 0; x <= 15; ++x) {
-+ for (int y = 0; y <= 15; ++y) {
-+ for (int z = 0; z <= 15; ++z) {
-+ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) {
-+ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F);
-+ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F);
-+ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F);
-+
-+ double mag = Math.sqrt(
-+ xDir * xDir + yDir * yDir + zDir * zDir
-+ );
-+
-+ rayCoords.add((xDir / mag) * (double)0.3F);
-+ rayCoords.add((yDir / mag) * (double)0.3F);
-+ rayCoords.add((zDir / mag) * (double)0.3F);
-+ }
-+ }
-+ }
-+ }
-+
-+ CACHED_RAYS = rayCoords.toDoubleArray();
-+ }
-+
-+ private static final int CHUNK_CACHE_SHIFT = 2;
-+ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1;
-+ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT;
-+
-+ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3;
-+ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1;
-+ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT;
-+
-+ // resistance = (res + 0.3F) * 0.3F;
-+ // so for resistance = 0, we need res = -0.3F
-+ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f);
-+ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache> blockCache = null;
-+ private long[] chunkPosCache = null;
-+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null;
-+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] directMappedBlockCache;
-+ private BlockPos.MutableBlockPos mutablePos;
-+
-+ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z,
-+ final long key, final boolean calculateResistance) {
-+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key);
-+ if (ret != null) {
-+ return ret;
-+ }
-+
-+ BlockPos pos = new BlockPos(x, y, z);
-+
-+ if (!this.level.isInWorldBounds(pos)) {
-+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true);
-+ } else {
-+ net.minecraft.world.level.chunk.LevelChunk chunk;
-+ long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4);
-+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT));
-+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) {
-+ chunk = this.chunkCache[chunkCacheKey];
-+ } else {
-+ this.chunkPosCache[chunkCacheKey] = chunkKey;
-+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4);
-+ }
-+
-+ BlockState blockState = ((ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z);
-+ FluidState fluidState = blockState.getFluidState();
-+
-+ Optional<Float> resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState);
-+
-+ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(
-+ key, pos, blockState, fluidState,
-+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f,
-+ false
-+ );
-+ }
-+
-+ this.blockCache.put(key, ret);
-+
-+ return ret;
-+ }
-+
-+ private boolean clipsAnything(final Vec3 from, final Vec3 to,
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context,
-+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
-+ final BlockPos.MutableBlockPos currPos) {
-+ // assume that context.delegated = false
-+ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
-+ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
-+ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
-+
-+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
-+ return false;
-+ }
-+
-+ final double toXAdj = to.x - adjX;
-+ final double toYAdj = to.y - adjY;
-+ final double toZAdj = to.z - adjZ;
-+ final double fromXAdj = from.x + adjX;
-+ final double fromYAdj = from.y + adjY;
-+ final double fromZAdj = from.z + adjZ;
-+
-+ int currX = Mth.floor(fromXAdj);
-+ int currY = Mth.floor(fromYAdj);
-+ int currZ = Mth.floor(fromZAdj);
-+
-+ final double diffX = toXAdj - fromXAdj;
-+ final double diffY = toYAdj - fromYAdj;
-+ final double diffZ = toZAdj - fromZAdj;
-+
-+ final double dxDouble = Math.signum(diffX);
-+ final double dyDouble = Math.signum(diffY);
-+ final double dzDouble = Math.signum(diffZ);
-+
-+ final int dx = (int)dxDouble;
-+ final int dy = (int)dyDouble;
-+ final int dz = (int)dzDouble;
-+
-+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
-+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
-+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
-+
-+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
-+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
-+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
-+
-+ for (;;) {
-+ currPos.set(currX, currY, currZ);
-+
-+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape
-+ // ClipContext.Fluid.NONE -> ignore fluids
-+
-+ // read block from cache
-+ final long key = BlockPos.asLong(currX, currY, currZ);
-+
-+ final int cacheKey =
-+ (currX & BLOCK_EXPLOSION_CACHE_MASK) |
-+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
-+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
-+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey];
-+ if (cachedBlock == null || cachedBlock.key != key) {
-+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false);
-+ }
-+
-+ final BlockState blockState = cachedBlock.blockState;
-+ if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyContextCollisionShape()) {
-+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape;
-+ if (collision == null) {
-+ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantContextCollisionShape();
-+ if (collision == null) {
-+ collision = blockState.getCollisionShape(this.level, currPos, context);
-+ if (!context.isDelegated()) {
-+ // if it was not delegated during this call, assume that for any future ones it will not be delegated
-+ // again, and cache the result
-+ cachedBlock.cachedCollisionShape = collision;
-+ }
-+ } else {
-+ cachedBlock.cachedCollisionShape = collision;
-+ }
-+ }
-+
-+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) {
-+ return true;
-+ }
-+ }
-+
-+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
-+ return false;
-+ }
-+
-+ // inc the smallest normalized coordinate
-+
-+ if (normalizedCurrX < normalizedCurrY) {
-+ if (normalizedCurrX < normalizedCurrZ) {
-+ currX += dx;
-+ normalizedCurrX += normalizedDiffX;
-+ } else {
-+ // x < y && x >= z <--> z < y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ } else if (normalizedCurrY < normalizedCurrZ) {
-+ // y <= x && y < z
-+ currY += dy;
-+ normalizedCurrY += normalizedDiffY;
-+ } else {
-+ // y <= x && z <= y <--> z <= y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ }
-+ }
-+
-+ private float getSeenFraction(final Vec3 source, final Entity target,
-+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache,
-+ final BlockPos.MutableBlockPos blockPos) {
-+ final AABB boundingBox = target.getBoundingBox();
-+ final double diffX = boundingBox.maxX - boundingBox.minX;
-+ final double diffY = boundingBox.maxY - boundingBox.minY;
-+ final double diffZ = boundingBox.maxZ - boundingBox.minZ;
-+
-+ final double incX = 1.0 / (diffX * 2.0 + 1.0);
-+ final double incY = 1.0 / (diffY * 2.0 + 1.0);
-+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0);
-+
-+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) {
-+ return 0.0f;
-+ }
-+
-+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX;
-+ final double offY = boundingBox.minY;
-+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ;
-+
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target);
-+
-+ int totalRays = 0;
-+ int missedRays = 0;
-+
-+ for (double dx = 0.0; dx <= 1.0; dx += incX) {
-+ final double fromX = Math.fma(dx, diffX, offX);
-+ for (double dy = 0.0; dy <= 1.0; dy += incY) {
-+ final double fromY = Math.fma(dy, diffY, offY);
-+ for (double dz = 0.0; dz <= 1.0; dz += incZ) {
-+ ++totalRays;
-+
-+ final Vec3 from = new Vec3(
-+ fromX,
-+ fromY,
-+ Math.fma(dz, diffZ, offZ)
-+ );
-+
-+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) {
-+ ++missedRays;
-+ }
-+ }
-+ }
-+ }
-+
-+ return (float)missedRays / (float)totalRays;
-+ }
-+ // Paper end - collisions optimisations
-
- public ServerExplosion(ServerLevel world, @Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Explosion.BlockInteraction destructionType) {
- this.level = world;
-@@ -127,65 +370,101 @@ public class ServerExplosion implements Explosion {
- }
-
- private List<BlockPos> calculateExplodedPositions() {
-- Set<BlockPos> set = new HashSet();
-- boolean flag = true;
--
-- for (int i = 0; i < 16; ++i) {
-- for (int j = 0; j < 16; ++j) {
-- for (int k = 0; k < 16; ++k) {
-- if (i == 0 || i == 15 || j == 0 || j == 15 || k == 0 || k == 15) {
-- double d0 = (double) ((float) i / 15.0F * 2.0F - 1.0F);
-- double d1 = (double) ((float) j / 15.0F * 2.0F - 1.0F);
-- double d2 = (double) ((float) k / 15.0F * 2.0F - 1.0F);
-- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2);
--
-- d0 /= d3;
-- d1 /= d3;
-- d2 /= d3;
-- float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
-- double d4 = this.center.x;
-- double d5 = this.center.y;
-- double d6 = this.center.z;
--
-- for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) {
-- BlockPos blockposition = BlockPos.containing(d4, d5, d6);
-- BlockState iblockdata = this.level.getBlockState(blockposition);
-- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed
-- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions
--
-- if (!this.level.isInWorldBounds(blockposition)) {
-- break;
-- }
-+ // Paper start - collision optimisations
-+ final ObjectArrayList<BlockPos> ret = new ObjectArrayList<>();
-
-- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid);
-+ final Vec3 center = this.center;
-
-- if (optional.isPresent()) {
-- f -= ((Float) optional.get() + 0.3F) * 0.3F;
-- }
-+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = this.directMappedBlockCache;
-
-- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
-- set.add(blockposition);
-- // Paper start - prevent headless pistons from forming
-- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
-- net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition);
-- if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
-- net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
-- set.add(blockposition.relative(direction.getOpposite()));
-- }
-+ // use initial cache value that is most likely to be used: the source position
-+ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache;
-+ {
-+ final int blockX = Mth.floor(center.x);
-+ final int blockY = Mth.floor(center.y);
-+ final int blockZ = Mth.floor(center.z);
-+
-+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
-+
-+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
-+ }
-+
-+ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of
-+ // a 16x16x16 cube
-+ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and
-+ // calculations in one go
-+ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most
-+ // block retrievals are not unique
-+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) {
-+ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache;
-+
-+ double currX = center.x;
-+ double currY = center.y;
-+ double currZ = center.z;
-+
-+ final double incX = CACHED_RAYS[ray];
-+ final double incY = CACHED_RAYS[ray + 1];
-+ final double incZ = CACHED_RAYS[ray + 2];
-+
-+ ray += 3;
-+
-+ float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F);
-+
-+ do {
-+ final int blockX = Mth.floor(currX);
-+ final int blockY = Mth.floor(currY);
-+ final int blockZ = Mth.floor(currZ);
-+
-+ final long key = BlockPos.asLong(blockX, blockY, blockZ);
-+
-+ if (cachedBlock.key != key) {
-+ final int cacheKey =
-+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) |
-+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) |
-+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT);
-+ cachedBlock = blockCache[cacheKey];
-+ if (cachedBlock == null || cachedBlock.key != key) {
-+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true);
-+ }
-+ }
-+
-+ if (cachedBlock.outOfWorld) {
-+ break;
-+ }
-+ final BlockState iblockdata = cachedBlock.blockState;
-+
-+ power -= cachedBlock.resistance;
-+
-+ if (power > 0.0f && cachedBlock.shouldExplode == null) {
-+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is.
-+ // basically, it is unused, which allows us to cache the result
-+ final boolean shouldExplode = iblockdata.isDestroyable() && this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power); // Paper - Protect Bedrock and End Portal/Frames from being destroyed
-+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE;
-+ if (shouldExplode) {
-+ if (this.fire || !cachedBlock.blockState.isAir()) {
-+ ret.add(cachedBlock.immutablePos);
-+ // Paper start - prevent headless pistons from forming
-+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) {
-+ net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(cachedBlock.immutablePos); // Paper - optimise collisions
-+ if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) {
-+ net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING);
-+ ret.add(cachedBlock.immutablePos.relative(direction.getOpposite())); // Paper - optimise collisions
- }
-- // Paper end - prevent headless pistons from forming
- }
--
-- d4 += d0 * 0.30000001192092896D;
-- d5 += d1 * 0.30000001192092896D;
-- d6 += d2 * 0.30000001192092896D;
-+ // Paper end - prevent headless pistons from forming
- }
- }
- }
-- }
-+
-+ power -= 0.22500001F;
-+ currX += incX;
-+ currY += incY;
-+ currZ += incZ;
-+ } while (power > 0.0f);
- }
-
-- return new ObjectArrayList(set);
-+ return ret;
-+ // Paper end - collision optimisations
- }
-
- private void hurtEntities() {
-@@ -391,6 +670,14 @@ public class ServerExplosion implements Explosion {
- return;
- }
- // CraftBukkit end
-+ // Paper start - collision optimisations
-+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>();
-+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
-+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS);
-+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH];
-+ this.directMappedBlockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH];
-+ this.mutablePos = new BlockPos.MutableBlockPos();
-+ // Paper end - collision optimisations
- this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center);
- List<BlockPos> list = this.calculateExplodedPositions();
-
-@@ -406,6 +693,13 @@ public class ServerExplosion implements Explosion {
- if (this.fire) {
- this.createFire(list);
- }
-+ // Paper start - collision optimisations
-+ this.blockCache = null;
-+ this.chunkPosCache = null;
-+ this.chunkCache = null;
-+ this.directMappedBlockCache = null;
-+ this.mutablePos = null;
-+ // Paper end - collision optimisations
-
- }
-
-@@ -499,12 +793,12 @@ public class ServerExplosion implements Explosion {
- // Paper start - Optimize explosions
- private float getBlockDensity(Vec3 vec3d, Entity entity) {
- if (!this.level.paperConfig().environment.optimizeExplosions) {
-- return getSeenPercent(vec3d, entity);
-+ return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
- }
- CacheKey key = new CacheKey(this, entity.getBoundingBox());
- Float blockDensity = this.level.explosionDensityCache.get(key);
- if (blockDensity == null) {
-- blockDensity = getSeenPercent(vec3d, entity);
-+ blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
- this.level.explosionDensityCache.put(key, blockDensity);
- }
-
-diff --git a/net/minecraft/world/level/biome/Biome.java b/net/minecraft/world/level/biome/Biome.java
-index 9f86b69d8c93a63e0b408ea52519f1fc2e798226..78afd8e51e03cd53c12b64db8a817da457f81bef 100644
---- a/net/minecraft/world/level/biome/Biome.java
-+++ b/net/minecraft/world/level/biome/Biome.java
-@@ -113,20 +113,7 @@ public final class Biome {
-
- @Deprecated
- public float getTemperature(BlockPos blockPos, int seaLevel) {
-- long l = blockPos.asLong();
-- Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get();
-- float f = long2FloatLinkedOpenHashMap.get(l);
-- if (!Float.isNaN(f)) {
-- return f;
-- } else {
-- float g = this.getHeightAdjustedTemperature(blockPos, seaLevel);
-- if (long2FloatLinkedOpenHashMap.size() == 1024) {
-- long2FloatLinkedOpenHashMap.removeFirstFloat();
-- }
--
-- long2FloatLinkedOpenHashMap.put(l, g);
-- return g;
-- }
-+ return this.getHeightAdjustedTemperature(blockPos, seaLevel); // Paper - optimise random ticking
- }
-
- public boolean shouldFreeze(LevelReader world, BlockPos blockPos) {
-diff --git a/net/minecraft/world/level/biome/BiomeManager.java b/net/minecraft/world/level/biome/BiomeManager.java
-index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644
---- a/net/minecraft/world/level/biome/BiomeManager.java
-+++ b/net/minecraft/world/level/biome/BiomeManager.java
-@@ -98,8 +98,7 @@ public class BiomeManager {
- }
-
- private static double getFiddle(long l) {
-- double d = (double)Math.floorMod(l >> 24, 1024) / 1024.0;
-- return (d - 0.5) * 0.9;
-+ return (double)(((l >> 24) & (1024 - 1)) - (1024/2)) * (0.9 / 1024.0); // Paper - avoid floorMod, fp division, and fp subtraction
- }
-
- public interface NoiseBiomeSource {
-diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java
-index 1aa69f4a7005242925124c74b8229e6fa7362717..c0b1f903962b25d8ff6c2b4fcd2be0e45de09b35 100644
---- a/net/minecraft/world/level/block/Block.java
-+++ b/net/minecraft/world/level/block/Block.java
-@@ -271,7 +271,7 @@ public class Block extends BlockBehaviour implements ItemLike {
- }
-
- public static boolean isShapeFullBlock(VoxelShape shape) {
-- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
-+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); // Paper - optimise collisions
- }
-
- public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {}
-diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java
-index b1101156b281d800f18b25208018722bbecded9f..8c0f332a1a0918f60226d969918ae7fe4fe74166 100644
---- a/net/minecraft/world/level/block/state/BlockBehaviour.java
-+++ b/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -797,7 +797,7 @@ public abstract class BlockBehaviour implements FeatureElement {
- boolean test(BlockState state, BlockGetter world, BlockPos pos);
- }
-
-- public abstract static class BlockStateBase extends StateHolder<Block, BlockState> {
-+ public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions
-
- private static final Direction[] DIRECTIONS = Direction.values();
- private static final VoxelShape[] EMPTY_OCCLUSION_SHAPES = (VoxelShape[]) Util.make(new VoxelShape[BlockBehaviour.BlockStateBase.DIRECTIONS.length], (avoxelshape) -> {
-@@ -841,6 +841,76 @@ public abstract class BlockBehaviour implements FeatureElement {
- private boolean propagatesSkylightDown;
- private int lightBlock;
-
-+ // Paper start - rewrite chunk system
-+ private boolean isConditionallyFullOpaque;
-+
-+ @Override
-+ public final boolean starlight$isConditionallyFullOpaque() {
-+ return this.isConditionallyFullOpaque;
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - optimise collisions
-+ private static final int RANDOM_OFFSET = 704237939;
-+ private static final Direction[] DIRECTIONS_CACHED = Direction.values();
-+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
-+ private final int id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+ private final int id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+ private boolean occludesFullBlock;
-+ private boolean emptyCollisionShape;
-+ private boolean emptyConstantCollisionShape;
-+ private VoxelShape constantCollisionShape;
-+
-+ private static void initCaches(final VoxelShape shape, final boolean neighbours) {
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock();
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$occludesFullBlock();
-+ shape.toAabbs();
-+ if (!shape.isEmpty()) {
-+ shape.bounds();
-+ }
-+ if (neighbours) {
-+ for (final Direction direction : DIRECTIONS_CACHED) {
-+ initCaches(((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction), false);
-+ initCaches(shape.getFaceShape(direction), false);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final boolean moonrise$hasCache() {
-+ return this.cache != null;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$occludesFullBlock() {
-+ return this.occludesFullBlock;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$emptyCollisionShape() {
-+ return this.emptyCollisionShape;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$emptyContextCollisionShape() {
-+ return this.emptyConstantCollisionShape;
-+ }
-+
-+ @Override
-+ public final int moonrise$uniqueId1() {
-+ return this.id1;
-+ }
-+
-+ @Override
-+ public final int moonrise$uniqueId2() {
-+ return this.id2;
-+ }
-+
-+ @Override
-+ public final VoxelShape moonrise$getConstantContextCollisionShape() {
-+ return this.constantCollisionShape;
-+ }
-+ // Paper end - optimise collisions
-+
- protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
- super(block, propertyMap, codec);
- this.fluidState = Fluids.EMPTY.defaultFluidState();
-@@ -925,6 +995,41 @@ public abstract class BlockBehaviour implements FeatureElement {
-
- this.propagatesSkylightDown = ((Block) this.owner).propagatesSkylightDown(this.asState());
- this.lightBlock = ((Block) this.owner).getLightBlock(this.asState());
-+ // Paper start - rewrite chunk system
-+ this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion;
-+ // Paper end - rewrite chunk system
-+ // Paper start - optimise collisions
-+ if (this.cache != null) {
-+ final VoxelShape collisionShape = this.cache.collisionShape;
-+ if (this.isAir()) {
-+ this.constantCollisionShape = Shapes.empty();
-+ } else {
-+ this.constantCollisionShape = null;
-+ }
-+ this.occludesFullBlock = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$occludesFullBlock();
-+ this.emptyCollisionShape = collisionShape.isEmpty();
-+ this.emptyConstantCollisionShape = this.constantCollisionShape != null && this.constantCollisionShape.isEmpty();
-+ // init caches
-+ initCaches(collisionShape, true);
-+ if (this.constantCollisionShape != null) {
-+ initCaches(this.constantCollisionShape, true);
-+ }
-+ } else {
-+ this.occludesFullBlock = false;
-+ this.emptyCollisionShape = false;
-+ this.emptyConstantCollisionShape = false;
-+ this.constantCollisionShape = null;
-+ }
-+
-+ if (this.occlusionShape != null) {
-+ initCaches(this.occlusionShape, true);
-+ }
-+ if (this.occlusionShapesByFace != null) {
-+ for (final VoxelShape shape : this.occlusionShapesByFace) {
-+ initCaches(shape, true);
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public Block getBlock() {
-diff --git a/net/minecraft/world/level/block/state/StateHolder.java b/net/minecraft/world/level/block/state/StateHolder.java
-index 422b364764e0df16ca250b4939d7b226e69c0840..815ee11aa5ed3448ff255e9c36d769478de477bd 100644
---- a/net/minecraft/world/level/block/state/StateHolder.java
-+++ b/net/minecraft/world/level/block/state/StateHolder.java
-@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
- import javax.annotation.Nullable;
- import net.minecraft.world.level.block.state.properties.Property;
-
--public abstract class StateHolder<O, S> {
-+public abstract class StateHolder<O, S> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccessStateHolder { // Paper - optimise blockstate property access
- public static final String NAME_TAG = "Name";
- public static final String PROPERTIES_TAG = "Properties";
- public static final Function<Entry<Property<?>, Comparable<?>>, String> PROPERTY_ENTRY_TO_STRING_FUNCTION = new Function<Entry<Property<?>, Comparable<?>>, String>() {
-@@ -34,14 +34,28 @@ public abstract class StateHolder<O, S> {
- }
- };
- protected final O owner;
-- private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values;
-+ private Reference2ObjectArrayMap<Property<?>, Comparable<?>> values; // Paper - optimise blockstate property access - remove final
- private Map<Property<?>, S[]> neighbours;
- protected final MapCodec<S> propertiesCodec;
-
-+ // Paper start - optimise blockstate property access
-+ protected ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> optimisedTable;
-+ protected final long tableIndex;
-+
-+ @Override
-+ public final long moonrise$getTableIndex() {
-+ return this.tableIndex;
-+ }
-+ // Paper end - optimise blockstate property access
-+
- protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) {
- this.owner = owner;
- this.values = propertyMap;
- this.propertiesCodec = codec;
-+ // Paper start - optimise blockstate property access
-+ this.optimisedTable = new ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<>(this.values.keySet());
-+ this.tableIndex = this.optimisedTable.getIndex((StateHolder<O, S>)(Object)this);
-+ // Paper end - optimise blockstate property access
- }
-
- public <T extends Comparable<T>> S cycle(Property<T> property) {
-@@ -67,20 +81,21 @@ public abstract class StateHolder<O, S> {
- }
-
- public Collection<Property<?>> getProperties() {
-- return Collections.unmodifiableCollection(this.values.keySet());
-+ return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access
- }
-
- public <T extends Comparable<T>> boolean hasProperty(Property<T> property) {
-- return this.values.containsKey(property);
-+ return property != null && this.optimisedTable.hasProperty(property); // Paper - optimise blockstate property access
- }
-
- public <T extends Comparable<T>> T getValue(Property<T> property) {
-- Comparable<?> comparable = this.values.get(property);
-- if (comparable == null) {
-- throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner);
-- } else {
-- return property.getValueClass().cast(comparable);
-+ // Paper start - optimise blockstate property access
-+ final T ret = this.optimisedTable.get(this.tableIndex, property);
-+ if (ret != null) {
-+ return ret;
- }
-+ throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner);
-+ // Paper end - optimise blockstate property access
- }
-
- public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) {
-@@ -93,22 +108,30 @@ public abstract class StateHolder<O, S> {
-
- @Nullable
- public <T extends Comparable<T>> T getNullableValue(Property<T> property) {
-- Comparable<?> comparable = this.values.get(property);
-- return comparable == null ? null : property.getValueClass().cast(comparable);
-+ return property == null ? null : this.optimisedTable.get(this.tableIndex, property); // Paper - optimise blockstate property access
- }
-
- public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) {
-- Comparable<?> comparable = this.values.get(property);
-- if (comparable == null) {
-- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner);
-- } else {
-- return this.setValueInternal(property, value, comparable);
-+ // Paper start - optimise blockstate property access
-+ final S ret = this.optimisedTable.set(this.tableIndex, property, value);
-+ if (ret != null) {
-+ return ret;
- }
-+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner);
-+ // Paper end - optimise blockstate property access
- }
-
- public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) {
-- Comparable<?> comparable = this.values.get(property);
-- return (S)(comparable == null ? this : this.setValueInternal(property, value, comparable));
-+ // Paper start - optimise blockstate property access
-+ if (property == null) {
-+ return (S)(StateHolder<O, S>)(Object)this;
-+ }
-+ final S ret = this.optimisedTable.trySet(this.tableIndex, property, value, (S)(StateHolder<O, S>)(Object)this);
-+ if (ret != null) {
-+ return ret;
-+ }
-+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner);
-+ // Paper end - optimise blockstate property access
- }
-
- private <T extends Comparable<T>, V extends T> S setValueInternal(Property<T> property, V newValue, Comparable<?> oldValue) {
-@@ -125,18 +148,27 @@ public abstract class StateHolder<O, S> {
- }
-
- public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) {
-- if (this.neighbours != null) {
-- throw new IllegalStateException();
-- } else {
-- Map<Property<?>, S[]> map = new Reference2ObjectArrayMap<>(this.values.size());
-+ // Paper start - optimise blockstate property access
-+ final Map<Map<Property<?>, Comparable<?>>, S> map = states;
-+ if (this.optimisedTable.isLoaded()) {
-+ return;
-+ }
-+ this.optimisedTable.loadInTable(map);
-
-- for (Entry<Property<?>, Comparable<?>> entry : this.values.entrySet()) {
-- Property<?> property = entry.getKey();
-- map.put(property, property.getPossibleValues().stream().map(value -> states.get(this.makeNeighbourValues(property, value))).toArray());
-- }
-+ // de-duplicate the tables
-+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
-+ final S value = entry.getValue();
-+ ((StateHolder<O, S>)value).optimisedTable = this.optimisedTable;
-+ }
-
-- this.neighbours = map;
-+ // remove values arrays
-+ for (final Map.Entry<Map<Property<?>, Comparable<?>>, S> entry : map.entrySet()) {
-+ final S value = entry.getValue();
-+ ((StateHolder<O, S>)value).values = null;
- }
-+
-+ return;
-+ // Paper end optimise blockstate property access
- }
-
- private Map<Property<?>, Comparable<?>> makeNeighbourValues(Property<?> property, Comparable<?> value) {
-@@ -146,7 +178,11 @@ public abstract class StateHolder<O, S> {
- }
-
- public Map<Property<?>, Comparable<?>> getValues() {
-- return this.values;
-+ // Paper start - optimise blockstate property access
-+ ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.util.ZeroCollidingReferenceStateTable<O, S> table = this.optimisedTable;
-+ // We have to use this.values until the table is loaded
-+ return table.isLoaded() ? table.getMapView(this.tableIndex) : this.values;
-+ // Paper end - optimise blockstate property access
- }
-
- protected static <O, S extends StateHolder<O, S>> Codec<S> codec(Codec<O> codec, Function<O, S> ownerToStateFunction) {
-diff --git a/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/net/minecraft/world/level/block/state/properties/BooleanProperty.java
-index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644
---- a/net/minecraft/world/level/block/state/properties/BooleanProperty.java
-+++ b/net/minecraft/world/level/block/state/properties/BooleanProperty.java
-@@ -3,13 +3,23 @@ package net.minecraft.world.level.block.state.properties;
- import java.util.List;
- import java.util.Optional;
-
--public final class BooleanProperty extends Property<Boolean> {
-+public final class BooleanProperty extends Property<Boolean> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Boolean> { // Paper - optimise blockstate property access
- private static final List<Boolean> VALUES = List.of(true, false);
- private static final int TRUE_INDEX = 0;
- private static final int FALSE_INDEX = 1;
-
-+ // Paper start - optimise blockstate property access
-+ private static final Boolean[] BY_ID = new Boolean[]{ Boolean.FALSE, Boolean.TRUE };
-+
-+ @Override
-+ public final int moonrise$getIdFor(final Boolean value) {
-+ return value.booleanValue() ? 1 : 0;
-+ }
-+ // Paper end - optimise blockstate property access
-+
- private BooleanProperty(String name) {
- super(name, Boolean.class);
-+ this.moonrise$setById(BY_ID); // Paper - optimise blockstate property access
- }
-
- @Override
-diff --git a/net/minecraft/world/level/block/state/properties/EnumProperty.java b/net/minecraft/world/level/block/state/properties/EnumProperty.java
-index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644
---- a/net/minecraft/world/level/block/state/properties/EnumProperty.java
-+++ b/net/minecraft/world/level/block/state/properties/EnumProperty.java
-@@ -10,11 +10,39 @@ import java.util.function.Predicate;
- import java.util.stream.Collectors;
- import net.minecraft.util.StringRepresentable;
-
--public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> {
-+public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access
- private final List<T> values;
- private final Map<String, T> names;
- private final int[] ordinalToIndex;
-
-+ // Paper start - optimise blockstate property access
-+ private int[] idLookupTable;
-+
-+ @Override
-+ public final int moonrise$getIdFor(final T value) {
-+ final Class<T> target = this.getValueClass();
-+ return ((value.getClass() != target && value.getDeclaringClass() != target)) ? -1 : this.idLookupTable[value.ordinal()];
-+ }
-+
-+ private void init() {
-+ final java.util.Collection<T> values = this.getPossibleValues();
-+ final Class<T> clazz = this.getValueClass();
-+
-+ int id = 0;
-+ this.idLookupTable = new int[clazz.getEnumConstants().length];
-+ Arrays.fill(this.idLookupTable, -1);
-+ final T[] byId = (T[])java.lang.reflect.Array.newInstance(clazz, values.size());
-+
-+ for (final T value : values) {
-+ final int valueId = id++;
-+ this.idLookupTable[value.ordinal()] = valueId;
-+ byId[valueId] = value;
-+ }
-+
-+ this.moonrise$setById(byId);
-+ }
-+ // Paper end - optimise blockstate property access
-+
- private EnumProperty(String name, Class<T> type, List<T> values) {
- super(name, type);
- if (values.isEmpty()) {
-@@ -37,6 +65,7 @@ public final class EnumProperty<T extends Enum<T> & StringRepresentable> extends
-
- this.names = builder.buildOrThrow();
- }
-+ this.init(); // Paper - optimise blockstate property access
- }
-
- @Override
-diff --git a/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644
---- a/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-+++ b/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-@@ -5,11 +5,33 @@ import java.util.List;
- import java.util.Optional;
- import java.util.stream.IntStream;
-
--public final class IntegerProperty extends Property<Integer> {
-+public final class IntegerProperty extends Property<Integer> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Integer> { // Paper - optimise blockstate property access
- private final IntImmutableList values;
- public final int min;
- public final int max;
-
-+ // Paper start - optimise blockstate property access
-+ @Override
-+ public final int moonrise$getIdFor(final Integer value) {
-+ final int val = value.intValue();
-+ final int ret = val - this.min;
-+
-+ return ret | ((this.max - ret) >> 31);
-+ }
-+
-+ private void init() {
-+ final int min = this.min;
-+ final int max = this.max;
-+
-+ final Integer[] byId = new Integer[max - min + 1];
-+ for (int i = min; i <= max; ++i) {
-+ byId[i - min] = Integer.valueOf(i);
-+ }
-+
-+ this.moonrise$setById(byId);
-+ }
-+ // Paper end - optimise blockstate property access
-+
- private IntegerProperty(String name, int min, int max) {
- super(name, Integer.class);
- if (min < 0) {
-@@ -21,6 +43,7 @@ public final class IntegerProperty extends Property<Integer> {
- this.max = max;
- this.values = IntImmutableList.toList(IntStream.range(min, max + 1));
- }
-+ this.init(); // Paper - optimise blockstate property access
- }
-
- @Override
-diff --git a/net/minecraft/world/level/block/state/properties/Property.java b/net/minecraft/world/level/block/state/properties/Property.java
-index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644
---- a/net/minecraft/world/level/block/state/properties/Property.java
-+++ b/net/minecraft/world/level/block/state/properties/Property.java
-@@ -10,7 +10,7 @@ import java.util.stream.Stream;
- import javax.annotation.Nullable;
- import net.minecraft.world.level.block.state.StateHolder;
-
--public abstract class Property<T extends Comparable<T>> {
-+public abstract class Property<T extends Comparable<T>> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<T> { // Paper - optimise blockstate property access
- private final Class<T> clazz;
- private final String name;
- @Nullable
-@@ -24,9 +24,38 @@ public abstract class Property<T extends Comparable<T>> {
- );
- private final Codec<Property.Value<T>> valueCodec = this.codec.xmap(this::value, Property.Value::value);
-
-+ // Paper start - optimise blockstate property access
-+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
-+ private final int id;
-+ private T[] byId;
-+
-+ @Override
-+ public final int moonrise$getId() {
-+ return this.id;
-+ }
-+
-+ @Override
-+ public final T moonrise$getById(final int id) {
-+ final T[] byId = this.byId;
-+ return id < 0 || id >= byId.length ? null : this.byId[id];
-+ }
-+
-+ @Override
-+ public final void moonrise$setById(final T[] byId) {
-+ if (this.byId != null) {
-+ throw new IllegalStateException();
-+ }
-+ this.byId = byId;
-+ }
-+
-+ @Override
-+ public abstract int moonrise$getIdFor(final T value);
-+ // Paper end - optimise blockstate property access
-+
- protected Property(String name, Class<T> type) {
- this.clazz = type;
- this.name = name;
-+ this.id = ID_GENERATOR.getAndIncrement(); // Paper - optimise blockstate property access
- }
-
- public Property.Value<T> value(T value) {
-diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java
-index 9d240aa87101662480cdd510839e017aa9c58fcd..f87abb22dd161b2b74401086de80dc95c9ac2dbb 100644
---- a/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -57,7 +57,7 @@ import net.minecraft.world.ticks.SavedTick;
- import net.minecraft.world.ticks.TickContainerAccess;
- import org.slf4j.Logger;
-
--public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess {
-+public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
-
- public static final int NO_FILLED_SECTION = -1;
- private static final Logger LOGGER = LogUtils.getLogger();
-@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
- @Nullable
- protected BlendingData blendingData;
- public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
-- protected ChunkSkyLightSources skyLightSources;
-+ // Paper - rewrite chunk system
- private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
- private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
- protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
-@@ -90,6 +90,57 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
- public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
- // CraftBukkit end
-
-+ // Paper start - rewrite chunk system
-+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles;
-+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles;
-+ private volatile boolean[] skyEmptinessMap;
-+ private volatile boolean[] blockEmptinessMap;
-+
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
-+ return this.blockNibbles;
-+ }
-+
-+ @Override
-+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
-+ this.blockNibbles = nibbles;
-+ }
-+
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
-+ return this.skyNibbles;
-+ }
-+
-+ @Override
-+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
-+ this.skyNibbles = nibbles;
-+ }
-+
-+ @Override
-+ public boolean[] starlight$getSkyEmptinessMap() {
-+ return this.skyEmptinessMap;
-+ }
-+
-+ @Override
-+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ this.skyEmptinessMap = emptinessMap;
-+ }
-+
-+ @Override
-+ public boolean[] starlight$getBlockEmptinessMap() {
-+ return this.blockEmptinessMap;
-+ }
-+
-+ @Override
-+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ this.blockEmptinessMap = emptinessMap;
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - get block chunk optimisation
-+ private final int minSection;
-+ private final int maxSection;
-+ // Paper end - get block chunk optimisation
-+
- public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
- this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
- this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
-@@ -99,7 +150,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
- this.inhabitedTime = inhabitedTime;
- this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
- this.blendingData = blendingData;
-- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
-+ // Paper - rewrite chunk system
- if (sectionArray != null) {
- if (this.sections.length == sectionArray.length) {
- System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
-@@ -111,6 +162,16 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
- this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method
- // CraftBukkit start
- this.biomeRegistry = biomeRegistry;
-+ // Paper start - rewrite chunk system
-+ if (!((Object)this instanceof ImposterProtoChunk)) {
-+ this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ }
-+ // Paper end - rewrite chunk system
-+ // Paper start - get block chunk optimisation
-+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(levelHeightAccessor);
-+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(levelHeightAccessor);
-+ // Paper end - get block chunk optimisation
- }
- public final Registry<Biome> biomeRegistry;
- // CraftBukkit end
-@@ -457,22 +518,22 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
-
- @Override
- public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) {
-- try {
-- int l = QuartPos.fromBlock(this.getMinY());
-- int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1;
-- int j1 = Mth.clamp(biomeY, l, i1);
-- int k1 = this.getSectionIndex(QuartPos.toBlock(j1));
--
-- return this.sections[k1].getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3);
-- } catch (Throwable throwable) {
-- CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting biome");
-- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being got");
--
-- crashreportsystemdetails.setDetail("Location", () -> {
-- return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ);
-- });
-- throw new ReportedException(crashreport);
-+ // Paper start - get block chunk optimisation
-+ int sectionY = (biomeY >> 2) - this.minSection;
-+ int rel = biomeY & 3;
-+
-+ final LevelChunkSection[] sections = this.sections;
-+
-+ if (sectionY < 0) {
-+ sectionY = 0;
-+ rel = 0;
-+ } else if (sectionY >= sections.length) {
-+ sectionY = sections.length - 1;
-+ rel = 3;
- }
-+
-+ return sections[sectionY].getNoiseBiome(biomeX & 3, rel, biomeZ & 3);
-+ // Paper end - get block chunk optimisation
- }
-
- // CraftBukkit start
-@@ -529,12 +590,12 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
- }
-
- public void initializeLightSources() {
-- this.skyLightSources.fillFrom(this);
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public ChunkSkyLightSources getSkyLightSources() {
-- return this.skyLightSources;
-+ return null; // Paper - rewrite chunk system
- }
-
- public static record PackedTicks(List<SavedTick<Block>> blocks, List<SavedTick<Fluid>> fluids) {
-diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java
-index ca6928f959eb63ac9183ba6c95738609839a7d32..e0cb360ece042c4fc6aa0d10106923fe25288f5c 100644
---- a/net/minecraft/world/level/chunk/ChunkGenerator.java
-+++ b/net/minecraft/world/level/chunk/ChunkGenerator.java
-@@ -120,7 +120,7 @@ public abstract class ChunkGenerator {
- return CompletableFuture.supplyAsync(() -> {
- chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler());
- return chunk;
-- }, Util.backgroundExecutor().forName("init_biomes"));
-+ }, Runnable::run); // Paper - rewrite chunk system
- }
-
- public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk);
-@@ -315,7 +315,7 @@ public abstract class ChunkGenerator {
- return Pair.of(placement.getLocatePos(pos), holder);
- }
-
-- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
-+ ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
-
- structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
- } while (structurestart == null);
-diff --git a/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644
---- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-+++ b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.material.FluidState;
- import net.minecraft.world.level.material.Fluids;
-
--public class EmptyLevelChunk extends LevelChunk {
-+public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
- private final Holder<Biome> biome;
-
- public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) {
-@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
- this.biome = biomeEntry;
- }
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
-+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
-+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public boolean[] starlight$getSkyEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {}
-+
-+ @Override
-+ public boolean[] starlight$getBlockEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {}
-+ // Paper end - rewrite chunk system
-+
- @Override
- public BlockState getBlockState(BlockPos pos) {
- return Blocks.VOID_AIR.defaultBlockState();
-diff --git a/net/minecraft/world/level/chunk/HashMapPalette.java b/net/minecraft/world/level/chunk/HashMapPalette.java
-index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644
---- a/net/minecraft/world/level/chunk/HashMapPalette.java
-+++ b/net/minecraft/world/level/chunk/HashMapPalette.java
-@@ -8,12 +8,19 @@ import net.minecraft.network.FriendlyByteBuf;
- import net.minecraft.network.VarInt;
- import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap;
-
--public class HashMapPalette<T> implements Palette<T> {
-+public class HashMapPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads
- private final IdMap<T> registry;
- private final CrudeIncrementalIntIdentityHashBiMap<T> values;
- private final PaletteResize<T> resizeHandler;
- private final int bits;
-
-+ // Paper start - optimise palette reads
-+ @Override
-+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) {
-+ return ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)this.values).moonrise$getRawPalette(container);
-+ }
-+ // Paper end - optimise palette reads
-+
- public HashMapPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> entries) {
- this(idList, bits, listener);
- entries.forEach(this.values::add);
-diff --git a/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-index f38700e5fbeeb8a913272d4464b8aa325d511dac..1eb8022f3e31603322e6c56516304afc9a11bbec 100644
---- a/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-+++ b/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-@@ -30,7 +30,7 @@ import net.minecraft.world.level.material.FluidState;
- import net.minecraft.world.ticks.BlackholeTickAccess;
- import net.minecraft.world.ticks.TickContainerAccess;
-
--public class ImposterProtoChunk extends ProtoChunk {
-+public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
- private final LevelChunk wrapped;
- private final boolean allowWrites;
-
-@@ -46,6 +46,48 @@ public class ImposterProtoChunk extends ProtoChunk {
- this.allowWrites = propagateToWrapped;
- }
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
-+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockNibbles();
-+ }
-+
-+ @Override
-+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
-+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyNibbles();
-+ }
-+
-+ @Override
-+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public boolean[] starlight$getSkyEmptinessMap() {
-+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyEmptinessMap(emptinessMap);
-+ }
-+
-+ @Override
-+ public boolean[] starlight$getBlockEmptinessMap() {
-+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockEmptinessMap(emptinessMap);
-+ }
-+ // Paper end - rewrite chunk system
-+
- @Nullable
- @Override
- public BlockEntity getBlockEntity(BlockPos pos) {
-diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
-index 0ade64bbdec563e555c981cee2208e6c72afe249..134d63076f231791988e67a5bdf191005112080b 100644
---- a/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -55,7 +55,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
- import net.minecraft.world.ticks.TickContainerAccess;
- import org.slf4j.Logger;
-
--public class LevelChunk extends ChunkAccess {
-+public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk, ca.spottedleaf.moonrise.patches.getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation
-
- static final Logger LOGGER = LogUtils.getLogger();
- private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
-@@ -114,6 +114,14 @@ public class LevelChunk extends ChunkAccess {
- this.postLoad = entityLoader;
- this.blockTicks = blockTickScheduler;
- this.fluidTicks = fluidTickScheduler;
-+ // Paper start - get block chunk optimisation
-+ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
-+ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(level);
-+
-+ final boolean empty = ((Object)this instanceof EmptyLevelChunk);
-+ this.debug = !empty && this.level.isDebug();
-+ this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE;
-+ // Paper end - get block chunk optimisation
- }
-
- // CraftBukkit start
-@@ -124,6 +132,39 @@ public class LevelChunk extends ChunkAccess {
- // Paper start
- boolean loadedTicketLevel;
- // 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();
-+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
-+ private static final BlockState VOID_AIR_BLOCKSTATE = Blocks.VOID_AIR.defaultBlockState();
-+ private final int minSection;
-+ private final int maxSection;
-+ private final boolean debug;
-+ private final BlockState defaultBlockState;
-+
-+ @Override
-+ public final BlockState moonrise$getBlock(final int x, final int y, final int z) {
-+ return this.getBlockStateFinal(x, y, z);
-+ }
-+ // Paper end - get block chunk optimisation
-
- public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
- this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
-@@ -157,13 +198,19 @@ public class LevelChunk extends ChunkAccess {
- }
- }
-
-- this.skyLightSources = protoChunk.skyLightSources;
-+ // Paper - rewrite chunk system
- this.setLightCorrect(protoChunk.isLightCorrect());
- this.markUnsaved();
- this.needsDecoration = true; // CraftBukkit
- // CraftBukkit start
- this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
- // CraftBukkit end
-+ // Paper start - rewrite chunk system
-+ this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles());
-+ this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles());
-+ this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap());
-+ this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap());
-+ // Paper end - rewrite chunk system
- }
-
- public void setUnsavedListener(LevelChunk.UnsavedListener unsavedListener) {
-@@ -366,7 +413,7 @@ public class LevelChunk extends ChunkAccess {
- ProfilerFiller gameprofilerfiller = Profiler.get();
-
- gameprofilerfiller.push("updateSkyLightSources");
-- this.skyLightSources.update(this, j, i, l);
-+ // Paper - rewrite chunk system
- gameprofilerfiller.popPush("queueCheckLight");
- this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
- gameprofilerfiller.pop();
-@@ -632,11 +679,12 @@ public class LevelChunk extends ChunkAccess {
-
- // CraftBukkit start
- public void loadCallback() {
-+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
- // Paper start
- this.loadedTicketLevel = true;
- // Paper end
- org.bukkit.Server server = this.level.getCraftServer();
-- this.level.getChunkSource().addLoadedChunk(this); // Paper
-+ // Paper - rewrite chunk system
- if (server != null) {
- /*
- * If it's a new world, the first few chunks are generated inside
-@@ -645,6 +693,7 @@ public class LevelChunk extends ChunkAccess {
- */
- org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
- server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
-+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.level, this.chunkPos, ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().getAllEntities()); // Paper - rewrite chunk system
-
- if (this.needsDecoration) {
- this.needsDecoration = false;
-@@ -671,13 +720,15 @@ public class LevelChunk extends ChunkAccess {
- }
-
- public void unloadCallback() {
-+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
- org.bukkit.Server server = this.level.getCraftServer();
-+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.level, this.chunkPos, ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().getAllEntities()); // Paper - rewrite chunk system
- org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
-- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
-+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below
- server.getPluginManager().callEvent(unloadEvent);
- // note: saving can be prevented, but not forced if no saving is actually required
- this.mustNotSave = !unloadEvent.isSaveChunk();
-- this.level.getChunkSource().removeLoadedChunk(this); // Paper
-+ // Paper - rewrite chunk system
- // Paper start
- this.loadedTicketLevel = false;
- // Paper end
-@@ -685,8 +736,31 @@ public class LevelChunk extends ChunkAccess {
-
- @Override
- public boolean isUnsaved() {
-- return super.isUnsaved() && !this.mustNotSave;
-+ // Paper start - rewrite chunk system
-+ final long gameTime = this.level.getGameTime();
-+ if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime)
-+ || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) {
-+ return true;
-+ }
-+
-+ return super.isUnsaved();
-+ // Paper end - rewrite chunk system
-+ }
-+
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public boolean tryMarkSaved() {
-+ if (!this.isUnsaved()) {
-+ return false;
-+ }
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty();
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty();
-+
-+ super.tryMarkSaved();
-+
-+ return true;
- }
-+ // Paper end - rewrite chunk system
- // CraftBukkit end
-
- public boolean isEmpty() {
-@@ -794,6 +868,7 @@ public class LevelChunk extends ChunkAccess {
-
- this.pendingBlockEntities.clear();
- this.upgradeData.upgrade(this);
-+ this.postProcessingDone = true; // Paper - rewrite chunk system
- }
-
- @Nullable
-diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java
-index 3dab36d00ea48101807ba40c7a7358b7eed12747..e4ae25c83ab9dd1aaa530a5456275ef63cdb8511 100644
---- a/net/minecraft/world/level/chunk/LevelChunkSection.java
-+++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.Blocks;
- import net.minecraft.world.level.block.state.BlockState;
- import net.minecraft.world.level.material.FluidState;
-
--public class LevelChunkSection {
-+public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection { // Paper - block counting
-
- public static final int SECTION_WIDTH = 16;
- public static final int SECTION_HEIGHT = 16;
-@@ -25,6 +25,30 @@ public class LevelChunkSection {
- public final PalettedContainer<BlockState> states;
- private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write
-
-+ // Paper start - block counting
-+ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16);
-+ static {
-+ for (short i = 0; i < (16*16*16); ++i) {
-+ FULL_LIST.add(i);
-+ }
-+ }
-+
-+ private boolean isClient;
-+ private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999;
-+ private short specialCollidingBlocks;
-+ private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList();
-+
-+ @Override
-+ public final boolean moonrise$hasSpecialCollidingBlocks() {
-+ return this.specialCollidingBlocks != 0;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() {
-+ return this.tickingBlocks;
-+ }
-+ // Paper end - block counting
-+
- private LevelChunkSection(LevelChunkSection section) {
- this.nonEmptyBlockCount = section.nonEmptyBlockCount;
- this.tickingBlockCount = section.tickingBlockCount;
-@@ -67,6 +91,45 @@ public class LevelChunkSection {
- return this.setBlockState(x, y, z, state, true);
- }
-
-+ // Paper start - block counting
-+ private void updateBlockCallback(final int x, final int y, final int z, final BlockState newState,
-+ final BlockState oldState) {
-+ if (oldState == newState) {
-+ return;
-+ }
-+
-+ if (this.isClient) {
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState)) {
-+ this.specialCollidingBlocks = CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS;
-+ }
-+ return;
-+ }
-+
-+ final boolean isSpecialOld = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(oldState);
-+ final boolean isSpecialNew = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState);
-+ if (isSpecialOld != isSpecialNew) {
-+ if (isSpecialOld) {
-+ --this.specialCollidingBlocks;
-+ } else {
-+ ++this.specialCollidingBlocks;
-+ }
-+ }
-+
-+ final boolean oldTicking = oldState.isRandomlyTicking();
-+ final boolean newTicking = newState.isRandomlyTicking();
-+ if (oldTicking != newTicking) {
-+ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks;
-+ final short position = (short)(x | (z << 4) | (y << (4+4)));
-+
-+ if (oldTicking) {
-+ tickingBlocks.remove(position);
-+ } else {
-+ tickingBlocks.add(position);
-+ }
-+ }
-+ }
-+ // Paper end - block counting
-+
- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
- BlockState iblockdata1;
-
-@@ -86,7 +149,7 @@ public class LevelChunkSection {
- }
- }
-
-- if (!fluid.isEmpty()) {
-+ if (!!fluid.isRandomlyTicking()) { // Paper - block counting
- --this.tickingFluidCount;
- }
-
-@@ -97,10 +160,12 @@ public class LevelChunkSection {
- }
- }
-
-- if (!fluid1.isEmpty()) {
-+ if (!!fluid1.isRandomlyTicking()) { // Paper - block counting
- ++this.tickingFluidCount;
- }
-
-+ this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting
-+
- return iblockdata1;
- }
-
-@@ -121,40 +186,70 @@ public class LevelChunkSection {
- }
-
- public void recalcBlockCounts() {
-- class a implements PalettedContainer.CountConsumer<BlockState> {
-+ // Paper start - block counting
-+ // reset, then recalculate
-+ this.nonEmptyBlockCount = (short)0;
-+ this.tickingBlockCount = (short)0;
-+ this.tickingFluidCount = (short)0;
-+ this.specialCollidingBlocks = (short)0;
-+ this.tickingBlocks.clear();
-+
-+ if (this.maybeHas((final BlockState state) -> !state.isAir())) {
-+ final PalettedContainer.Data<BlockState> data = this.states.data;
-+ final Palette<BlockState> palette = data.palette();
-+ final int paletteSize = palette.getSize();
-+ final net.minecraft.util.BitStorage storage = data.storage();
-+
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> counts;
-+ if (paletteSize == 1) {
-+ counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1);
-+ counts.put(0, FULL_LIST);
-+ } else {
-+ counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries();
-+ }
-+
-+ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.shorts.ShortArrayList> entry = iterator.next();
-+ final int paletteIdx = entry.getIntKey();
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue();
-+ final int paletteCount = coordinates.size();
-
-- public int nonEmptyBlockCount;
-- public int tickingBlockCount;
-- public int tickingFluidCount;
-+ final BlockState state = palette.valueFor(paletteIdx);
-
-- a(final LevelChunkSection chunksection) {}
-+ if (state.isAir()) {
-+ continue;
-+ }
-+
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
-+ this.specialCollidingBlocks += (short)paletteCount;
-+ }
-+ this.nonEmptyBlockCount += (short)paletteCount;
-+ if (state.isRandomlyTicking()) {
-+ this.tickingBlockCount += (short)paletteCount;
-+ final short[] raw = coordinates.elements();
-+ final int rawLen = raw.length;
-+
-+ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks;
-
-- public void accept(BlockState iblockdata, int i) {
-- FluidState fluid = iblockdata.getFluidState();
-+ tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16));
-
-- if (!iblockdata.isAir()) {
-- this.nonEmptyBlockCount += i;
-- if (iblockdata.isRandomlyTicking()) {
-- this.tickingBlockCount += i;
-+ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length);
-+ for (int i = 0; i < paletteCount; ++i) {
-+ tickingBlocks.add(raw[i]);
- }
- }
-
-+ final FluidState fluid = state.getFluidState();
-+
- if (!fluid.isEmpty()) {
-- this.nonEmptyBlockCount += i;
-+ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct
- if (fluid.isRandomlyTicking()) {
-- this.tickingFluidCount += i;
-+ this.tickingFluidCount += (short)paletteCount;
- }
- }
--
- }
- }
--
-- a a0 = new a(this);
--
-- this.states.count(a0);
-- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount;
-- this.tickingBlockCount = (short) a0.tickingBlockCount;
-- this.tickingFluidCount = (short) a0.tickingFluidCount;
-+ // Paper end - block counting
- }
-
- public PalettedContainer<BlockState> getStates() {
-@@ -172,6 +267,11 @@ public class LevelChunkSection {
-
- datapaletteblock.read(buf);
- this.biomes = datapaletteblock;
-+ // Paper start - block counting
-+ this.isClient = true;
-+ // force has special colliding blocks to be true
-+ this.specialCollidingBlocks = this.nonEmptyBlockCount != (short)0 && this.maybeHas(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil::isSpecialCollidingBlock) ? CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS : (short)0;
-+ // Paper end - block counting
- }
-
- public void readBiomes(FriendlyByteBuf buf) {
-diff --git a/net/minecraft/world/level/chunk/LinearPalette.java b/net/minecraft/world/level/chunk/LinearPalette.java
-index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644
---- a/net/minecraft/world/level/chunk/LinearPalette.java
-+++ b/net/minecraft/world/level/chunk/LinearPalette.java
-@@ -7,13 +7,20 @@ import net.minecraft.network.FriendlyByteBuf;
- import net.minecraft.network.VarInt;
- import org.apache.commons.lang3.Validate;
-
--public class LinearPalette<T> implements Palette<T> {
-+public class LinearPalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads
- private final IdMap<T> registry;
- private final T[] values;
- private final PaletteResize<T> resizeHandler;
- private final int bits;
- private int size;
-
-+ // Paper start - optimise palette reads
-+ @Override
-+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) {
-+ return this.values;
-+ }
-+ // Paper end - optimise palette reads
-+
- private LinearPalette(IdMap<T> idList, int bits, PaletteResize<T> listener, List<T> list) {
- this.registry = idList;
- this.values = (T[])(new Object[1 << bits]);
-diff --git a/net/minecraft/world/level/chunk/Palette.java b/net/minecraft/world/level/chunk/Palette.java
-index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644
---- a/net/minecraft/world/level/chunk/Palette.java
-+++ b/net/minecraft/world/level/chunk/Palette.java
-@@ -5,7 +5,7 @@ import java.util.function.Predicate;
- import net.minecraft.core.IdMap;
- import net.minecraft.network.FriendlyByteBuf;
-
--public interface Palette<T> {
-+public interface Palette<T> extends ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads
- int idFor(T object);
-
- boolean maybeHas(Predicate<T> predicate);
-diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java
-index 69d6f203366df658e1ade55d917f0aa2b8a49be9..8b84bf2272556ac3321cbf16361d7f48a1cc6873 100644
---- a/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -29,7 +29,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0;
- public final IdMap<T> registry;
- private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
-- private volatile PalettedContainer.Data<T> data;
-+ public volatile PalettedContainer.Data<T> data; // Paper - optimise collisions - public
- private final PalettedContainer.Strategy strategy;
- // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
-
-@@ -77,6 +77,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- );
- }
-
-+ // Paper start - optimise palette reads
-+ private void updateData(final PalettedContainer.Data<T> data) {
-+ if (data != null) {
-+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$setPalette(
-+ ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data)
-+ );
-+ }
-+ }
-+
-+ private T readPaletteSlow(final PalettedContainer.Data<T> data, final int paletteIdx) {
-+ return data.palette.valueFor(paletteIdx);
-+ }
-+
-+ private T readPalette(final PalettedContainer.Data<T> data, final int paletteIdx) {
-+ final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$getPalette();
-+ if (palette == null) {
-+ return this.readPaletteSlow(data, paletteIdx);
-+ }
-+
-+ final T ret = palette[paletteIdx];
-+ if (ret == null) {
-+ throw new IllegalArgumentException("Palette index out of bounds");
-+ }
-+ return ret;
-+ }
-+ // Paper end - optimise palette reads
-+
- // Paper start - Anti-Xray - Add preset values
- @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); }
- public PalettedContainer(
-@@ -113,6 +140,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- }
- }
- // Paper end
-+ this.updateData(this.data); // Paper - optimise palette reads
- }
-
- // Paper start - Anti-Xray - Add preset values
-@@ -122,6 +150,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- this.registry = idList;
- this.strategy = paletteProvider;
- this.data = data;
-+ this.updateData(this.data); // Paper - optimise palette reads
- }
-
- private PalettedContainer(PalettedContainer<T> container, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
-@@ -140,6 +169,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- this.registry = idList;
- this.data = this.createOrReuseData(null, 0);
- this.data.palette.idFor(object);
-+ this.updateData(this.data); // Paper - optimise palette reads
- }
-
- private PalettedContainer.Data<T> createOrReuseData(@Nullable PalettedContainer.Data<T> previousData, int bits) {
-@@ -166,6 +196,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- data2.copyFrom(data.palette, data.storage);
- this.data = data2;
- this.addPresetValues();
-+ this.updateData(this.data); // Paper - optimise palette reads
- return object == null ? -1 : data2.palette.idFor(object);
- // Paper end
- }
-@@ -198,9 +229,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- }
-
- private synchronized T getAndSet(int index, T value) { // Paper - synchronize
-- int i = this.data.palette.idFor(value);
-- int j = this.data.storage.getAndSet(index, i);
-- return this.data.palette.valueFor(j);
-+ // Paper start - optimise palette reads
-+ final int paletteIdx = this.data.palette.idFor(value);
-+ final PalettedContainer.Data<T> data = this.data;
-+ final int prev = data.storage.getAndSet(index, paletteIdx);
-+ return this.readPalette(data, prev);
-+ // Paper end - optimise palette reads
- }
-
- public void set(int x, int y, int z, T value) {
-@@ -223,9 +257,11 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- return this.get(this.strategy.getIndex(x, y, z));
- }
-
-- protected T get(int index) {
-- PalettedContainer.Data<T> data = this.data;
-- return data.palette.valueFor(data.storage.get(index));
-+ public T get(int index) { // Paper - public
-+ // Paper start - optimise palette reads
-+ final PalettedContainer.Data<T> data = this.data;
-+ return this.readPalette(data, data.storage.get(index));
-+ // Paper end - optimise palette reads
- }
-
- @Override
-@@ -246,6 +282,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- buf.readLongArray(data.storage.getRaw());
- this.data = data;
- this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
-+ this.updateData(this.data); // Paper - optimise palette reads
- } finally {
- this.release();
- }
-@@ -394,7 +431,44 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- void accept(T object, int count);
- }
-
-- static record Data<T>(PalettedContainer.Configuration<T> configuration, BitStorage storage, Palette<T> palette) {
-+ // Paper start - optimise palette reads
-+ public static final class Data<T> implements ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> {
-+
-+ private final PalettedContainer.Configuration<T> configuration;
-+ private final BitStorage storage;
-+ private final Palette<T> palette;
-+
-+ private T[] moonrise$palette;
-+
-+ public Data(final PalettedContainer.Configuration<T> configuration, final BitStorage storage, final Palette<T> palette) {
-+ this.configuration = configuration;
-+ this.storage = storage;
-+ this.palette = palette;
-+ }
-+
-+ public PalettedContainer.Configuration<T> configuration() {
-+ return this.configuration;
-+ }
-+
-+ public BitStorage storage() {
-+ return this.storage;
-+ }
-+
-+ public Palette<T> palette() {
-+ return this.palette;
-+ }
-+
-+ @Override
-+ public final T[] moonrise$getPalette() {
-+ return this.moonrise$palette;
-+ }
-+
-+ @Override
-+ public final void moonrise$setPalette(final T[] palette) {
-+ this.moonrise$palette = palette;
-+ }
-+ // Paper end - optimise palette reads
-+
- public void copyFrom(Palette<T> palette, BitStorage storage) {
- for (int i = 0; i < storage.getSize(); i++) {
- T object = palette.valueFor(storage.get(i));
-diff --git a/net/minecraft/world/level/chunk/ProtoChunk.java b/net/minecraft/world/level/chunk/ProtoChunk.java
-index 15e14f5d006389c823fa6baf8c1a4f22804d4aa8..759adee51bad99bd4bbee4f44247e8c8486cfbd6 100644
---- a/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -149,7 +149,7 @@ public class ProtoChunk extends ChunkAccess {
- }
-
- if (LightEngine.hasDifferentLightProperties(blockState, state)) {
-- this.skyLightSources.update(this, m, j, o);
-+ // Paper - rewrite chunk system
- this.lightEngine.checkBlock(pos);
- }
- }
-diff --git a/net/minecraft/world/level/chunk/SingleValuePalette.java b/net/minecraft/world/level/chunk/SingleValuePalette.java
-index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644
---- a/net/minecraft/world/level/chunk/SingleValuePalette.java
-+++ b/net/minecraft/world/level/chunk/SingleValuePalette.java
-@@ -8,12 +8,24 @@ import net.minecraft.network.FriendlyByteBuf;
- import net.minecraft.network.VarInt;
- import org.apache.commons.lang3.Validate;
-
--public class SingleValuePalette<T> implements Palette<T> {
-+public class SingleValuePalette<T> implements Palette<T>, ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T> { // Paper - optimise palette reads
- private final IdMap<T> registry;
- @Nullable
- private T value;
- private final PaletteResize<T> resizeHandler;
-
-+ // Paper start - optimise palette reads
-+ private T[] rawPalette;
-+
-+ @Override
-+ public final T[] moonrise$getRawPalette(final ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T> container) {
-+ if (this.rawPalette != null) {
-+ return this.rawPalette;
-+ }
-+ return this.rawPalette = (T[])new Object[] { this.value };
-+ }
-+ // Paper end - optimise palette reads
-+
- public SingleValuePalette(IdMap<T> idList, PaletteResize<T> listener, List<T> entries) {
- this.registry = idList;
- this.resizeHandler = listener;
-@@ -33,6 +45,11 @@ public class SingleValuePalette<T> implements Palette<T> {
- return this.resizeHandler.onResize(1, object);
- } else {
- this.value = object;
-+ // Paper start - optimise palette reads
-+ if (this.rawPalette != null) {
-+ this.rawPalette[0] = object;
-+ }
-+ // Paper end - optimise palette reads
- return 0;
- }
- }
-@@ -58,6 +75,11 @@ public class SingleValuePalette<T> implements Palette<T> {
- @Override
- public void read(FriendlyByteBuf buf) {
- this.value = this.registry.byIdOrThrow(buf.readVarInt());
-+ // Paper start - optimise palette reads
-+ if (this.rawPalette != null) {
-+ this.rawPalette[0] = this.value;
-+ }
-+ // Paper end - optimise palette reads
- }
-
- @Override
-diff --git a/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644
---- a/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-+++ b/net/minecraft/world/level/chunk/status/ChunkPyramid.java
-@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
- .step(ChunkStatus.CARVERS, builder -> builder)
- .step(ChunkStatus.FEATURES, builder -> builder)
- .step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight))
-- .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light))
-+ .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours
- .step(ChunkStatus.SPAWN, builder -> builder)
- .step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full))
- .build();
-diff --git a/net/minecraft/world/level/chunk/status/ChunkStatus.java b/net/minecraft/world/level/chunk/status/ChunkStatus.java
-index 4f84ff9cdb3303251e035a12ce9d8b9a0b58f46e..d80b7d555e02d1d4b82945373d383eaedbf4b976 100644
---- a/net/minecraft/world/level/chunk/status/ChunkStatus.java
-+++ b/net/minecraft/world/level/chunk/status/ChunkStatus.java
-@@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation;
- import net.minecraft.world.level.levelgen.Heightmap;
- import org.jetbrains.annotations.VisibleForTesting;
-
--public class ChunkStatus {
-+public class ChunkStatus implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus { // Paper - rewrite chunk system
- public static final int MAX_STRUCTURE_DISTANCE = 8;
- private static final EnumSet<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
- public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of(
-@@ -51,8 +51,68 @@ public class ChunkStatus {
- return list;
- }
-
-+ // Paper start - rewrite chunk system
-+ private boolean isParallelCapable;
-+ private boolean emptyLoadTask;
-+ private int writeRadius;
-+ private ChunkStatus nextStatus;
-+ private java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete;
-+
-+ @Override
-+ public final boolean moonrise$isParallelCapable() {
-+ return this.isParallelCapable;
-+ }
-+
-+ @Override
-+ public final void moonrise$setParallelCapable(final boolean value) {
-+ this.isParallelCapable = value;
-+ }
-+
-+ @Override
-+ public final int moonrise$getWriteRadius() {
-+ return this.writeRadius;
-+ }
-+
-+ @Override
-+ public final void moonrise$setWriteRadius(final int value) {
-+ this.writeRadius = value;
-+ }
-+
-+ @Override
-+ public final ChunkStatus moonrise$getNextStatus() {
-+ return this.nextStatus;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$isEmptyLoadStatus() {
-+ return this.emptyLoadTask;
-+ }
-+
-+ @Override
-+ public void moonrise$setEmptyLoadStatus(final boolean value) {
-+ this.emptyLoadTask = value;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$isEmptyGenStatus() {
-+ return (Object)this == ChunkStatus.EMPTY;
-+ }
-+
-+ @Override
-+ public final java.util.concurrent.atomic.AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() {
-+ return this.warnedAboutNoImmediateComplete;
-+ }
-+ // Paper end - rewrite chunk system
-+
- @VisibleForTesting
- protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) {
-+ this.isParallelCapable = false;
-+ this.writeRadius = -1;
-+ this.nextStatus = (ChunkStatus)(Object)this;
-+ if (previous != null) {
-+ previous.nextStatus = (ChunkStatus)(Object)this;
-+ }
-+ this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
- this.parent = previous == null ? this : previous;
- this.chunkType = chunkType;
- this.heightmapsAfter = heightMapTypes;
-diff --git a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-index 3d8a35d8cf29447ee7ac750dbc6ffcdb0f89b81b..9a3900e970f22892d8a3da8a28f922aa9b62765f 100644
---- a/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-+++ b/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
-@@ -152,7 +152,7 @@ public class ChunkStatusTasks {
- chunk1 = protochunkextension.getWrapped();
- } else {
- chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
-- ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
-+ ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities(), protochunk.getPos()); // Paper - rewrite chunk system
- });
- generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
- }
-@@ -168,7 +168,7 @@ public class ChunkStatusTasks {
- }, context.mainThreadExecutor());
- }
-
-- public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public
-+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public // Paper - rewrite chunk system - add ChunkPos param
- if (!entities.isEmpty()) {
- // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
- world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world, EntitySpawnReason.LOAD).filter((entity) -> {
-@@ -180,7 +180,7 @@ public class ChunkStatusTasks {
- }
- checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
- return !needsRemoval;
-- }));
-+ }), pos); // Paper - rewrite chunk system
- // CraftBukkit end
- }
-
-diff --git a/net/minecraft/world/level/chunk/status/ChunkStep.java b/net/minecraft/world/level/chunk/status/ChunkStep.java
-index 3d37a0372cdd99e806a9651cc1cabaefa9338065..f9aad1b8c02b70e620efdc2a58cadf4fff0f3ed5 100644
---- a/net/minecraft/world/level/chunk/status/ChunkStep.java
-+++ b/net/minecraft/world/level/chunk/status/ChunkStep.java
-@@ -11,9 +11,50 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
- import net.minecraft.world.level.chunk.ChunkAccess;
- import net.minecraft.world.level.chunk.ProtoChunk;
-
--public record ChunkStep(
-- ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
--) {
-+// Paper start - rewerite chunk system - convert record to class
-+public final class ChunkStep implements ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep { // Paper - rewrite chunk system
-+ private final ChunkStatus targetStatus;
-+ private final ChunkDependencies directDependencies;
-+ private final ChunkDependencies accumulatedDependencies;
-+ private final int blockStateWriteRadius;
-+ private final ChunkStatusTask task;
-+
-+ private final ChunkStatus[] byRadius; // Paper - rewrite chunk system
-+
-+ public ChunkStep(
-+ ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
-+ ) {
-+ this.targetStatus = targetStatus;
-+ this.directDependencies = directDependencies;
-+ this.accumulatedDependencies = accumulatedDependencies;
-+ this.blockStateWriteRadius = blockStateWriteRadius;
-+ this.task = task;
-+
-+ // Paper start - rewrite chunk system
-+ this.byRadius = new ChunkStatus[this.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + 1];
-+ this.byRadius[0] = targetStatus.getParent();
-+
-+ for (ChunkStatus status = targetStatus.getParent(); status != ChunkStatus.EMPTY; status = status.getParent()) {
-+ final int radius = this.getAccumulatedRadiusOf(status);
-+
-+ for (int j = 0; j <= radius; ++j) {
-+ if (this.byRadius[j] == null) {
-+ this.byRadius[j] = status;
-+ }
-+ }
-+ }
-+ // Paper end - rewrite chunk system
-+ }
-+
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius) {
-+ return this.byRadius[radius];
-+ }
-+ // Paper end - rewrite chunk system
-+
-+ // Paper start - rewerite chunk system - convert record to class
-+
- public int getAccumulatedRadiusOf(ChunkStatus status) {
- return status == this.targetStatus ? 0 : this.accumulatedDependencies.getRadiusOf(status);
- }
-@@ -39,6 +80,56 @@ public record ChunkStep(
- return chunk;
- }
-
-+ // Paper start - rewerite chunk system - convert record to class
-+ public ChunkStatus targetStatus() {
-+ return targetStatus;
-+ }
-+
-+ public ChunkDependencies directDependencies() {
-+ return directDependencies;
-+ }
-+
-+ public ChunkDependencies accumulatedDependencies() {
-+ return accumulatedDependencies;
-+ }
-+
-+ public int blockStateWriteRadius() {
-+ return blockStateWriteRadius;
-+ }
-+
-+ public ChunkStatusTask task() {
-+ return task;
-+ }
-+
-+ @Override
-+ public boolean equals(Object obj) {
-+ if (obj == this) return true;
-+ if (obj == null || obj.getClass() != this.getClass()) return false;
-+ var that = (net.minecraft.world.level.chunk.status.ChunkStep) obj;
-+ return java.util.Objects.equals(this.targetStatus, that.targetStatus) &&
-+ java.util.Objects.equals(this.directDependencies, that.directDependencies) &&
-+ java.util.Objects.equals(this.accumulatedDependencies, that.accumulatedDependencies) &&
-+ this.blockStateWriteRadius == that.blockStateWriteRadius &&
-+ java.util.Objects.equals(this.task, that.task);
-+ }
-+
-+ @Override
-+ public int hashCode() {
-+ return java.util.Objects.hash(targetStatus, directDependencies, accumulatedDependencies, blockStateWriteRadius, task);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "ChunkStep[" +
-+ "targetStatus=" + targetStatus + ", " +
-+ "directDependencies=" + directDependencies + ", " +
-+ "accumulatedDependencies=" + accumulatedDependencies + ", " +
-+ "blockStateWriteRadius=" + blockStateWriteRadius + ", " +
-+ "task=" + task + ']';
-+ }
-+ // Paper end - rewerite chunk system - convert record to class
-+
-+
- public static class Builder {
- private final ChunkStatus status;
- @Nullable
-diff --git a/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-index 092f7b6bba4e1291f76c2c09155f33803e93eb04..46f4b6706a1ca24ff6fc28960ad01a067109819f 100644
---- a/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/ChunkStorage.java
-@@ -28,21 +28,31 @@ import net.minecraft.world.level.dimension.LevelStem;
- import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
- import net.minecraft.world.level.storage.DimensionDataStorage;
-
--public class ChunkStorage implements AutoCloseable {
-+public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage { // Paper - rewrite chunk system
-
- public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
-- private final IOWorker worker;
-+ // Paper - rewrite chunk system
- protected final DataFixer fixerUpper;
- @Nullable
- private volatile LegacyStructureDataHandler legacyStructureHandler;
-
-+ // Paper start - rewrite chunk system
-+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger();
-+ private final RegionFileStorage storage;
-+
-+ @Override
-+ public final RegionFileStorage moonrise$getRegionStorage() {
-+ return this.storage;
-+ }
-+ // Paper end - rewrite chunk system
-+
- public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) {
- this.fixerUpper = dataFixer;
-- this.worker = new IOWorker(storageKey, directory, dsync);
-+ this.storage = new IOWorker(storageKey, directory, dsync).storage; // Paper - rewrite chunk system
- }
-
- public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
-- return this.worker.isOldChunkAround(chunkPos, checkRadius);
-+ return true; // Paper - rewrite chunk system
- }
-
- // CraftBukkit start
-@@ -102,7 +112,9 @@ public class ChunkStorage implements AutoCloseable {
- if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
- LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
-
-+ synchronized (persistentstructurelegacy) { // Paper - rewrite chunk system
- nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
-+ } // Paper - rewrite chunk system
- }
- }
-
-@@ -169,7 +181,13 @@ public class ChunkStorage implements AutoCloseable {
- }
-
- public CompletableFuture<Optional<CompoundTag>> read(ChunkPos chunkPos) {
-- return this.worker.loadAsync(chunkPos);
-+ // Paper start - rewrite chunk system
-+ try {
-+ return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos)));
-+ } catch (final Throwable throwable) {
-+ return CompletableFuture.failedFuture(throwable);
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- public CompletableFuture<Void> write(ChunkPos chunkPos, Supplier<CompoundTag> nbtSupplier) {
-@@ -185,29 +203,54 @@ public class ChunkStorage implements AutoCloseable {
- };
- // Paper end - guard against possible chunk pos desync
- this.handleLegacyStructureIndex(chunkPos);
-- return this.worker.store(chunkPos, guardedPosCheck); // Paper - guard against possible chunk pos desync
-+ // Paper start - rewrite chunk system
-+ try {
-+ this.storage.write(chunkPos, guardedPosCheck.get());
-+ return CompletableFuture.completedFuture(null);
-+ } catch (final Throwable throwable) {
-+ return CompletableFuture.failedFuture(throwable);
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
- if (this.legacyStructureHandler != null) {
-+ synchronized (this.legacyStructureHandler) { // Paper - rewrite chunk system
- this.legacyStructureHandler.removeIndex(chunkPos.toLong());
-+ } // Paper - rewrite chunk system
- }
-
- }
-
- public void flushWorker() {
-- this.worker.synchronize(true).join();
-+ // Paper start - rewrite chunk system
-+ try {
-+ this.storage.flush();
-+ } catch (final IOException ex) {
-+ LOGGER.error("Failed to flush chunk storage", ex);
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- public void close() throws IOException {
-- this.worker.close();
-+ this.storage.close(); // Paper - rewrite chunk system
- }
-
- public ChunkScanAccess chunkScanner() {
-- return this.worker;
-+ // Paper start - rewrite chunk system
-+ // TODO ChunkMap implementation?
-+ return (chunkPos, streamTagVisitor) -> {
-+ try {
-+ this.storage.scanChunk(chunkPos, streamTagVisitor);
-+ return java.util.concurrent.CompletableFuture.completedFuture(null);
-+ } catch (IOException e) {
-+ throw new RuntimeException(e);
-+ }
-+ };
-+ // Paper end - rewrite chunk system
- }
-
-- protected RegionStorageInfo storageInfo() {
-- return this.worker.storageInfo();
-+ public RegionStorageInfo storageInfo() { // Paper - public
-+ return this.storage.info(); // Paper - rewrite chunk system
- }
- }
-diff --git a/net/minecraft/world/level/chunk/storage/EntityStorage.java b/net/minecraft/world/level/chunk/storage/EntityStorage.java
-index a0cbccd2cf1ac785745d86c42b6f58fb8bad7ffa..16ca1c8672e5f0a27f8a30498c754a81cdec5191 100644
---- a/net/minecraft/world/level/chunk/storage/EntityStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/EntityStorage.java
-@@ -71,12 +71,12 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
- }
- }
-
-- private static ChunkPos readChunkPos(CompoundTag chunkNbt) {
-+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public
- int[] is = chunkNbt.getIntArray("Position");
- return new ChunkPos(is[0], is[1]);
- }
-
-- private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) {
-+ public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public
- chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
- }
-
-diff --git a/net/minecraft/world/level/chunk/storage/IOWorker.java b/net/minecraft/world/level/chunk/storage/IOWorker.java
-index 1f2997cf5367200084f32c437f77040c8c6a18e6..a8a9e59a9721a76e34f78c1baa5026e5fe1d2bda 100644
---- a/net/minecraft/world/level/chunk/storage/IOWorker.java
-+++ b/net/minecraft/world/level/chunk/storage/IOWorker.java
-@@ -30,7 +30,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
- private static final Logger LOGGER = LogUtils.getLogger();
- private final AtomicBoolean shutdownRequested = new AtomicBoolean();
- private final PriorityConsecutiveExecutor consecutiveExecutor;
-- private final RegionFileStorage storage;
-+ public final RegionFileStorage storage; // Paper - public
- private final SequencedMap<ChunkPos, IOWorker.PendingStore> pendingWrites = new LinkedHashMap<>();
- private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>();
- private static final int REGION_CACHE_SIZE = 1024;
-diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
-index 863960ead8deaa0553be1c98e4fa09f07fcb8ef0..057875cbbdc92ba49b429f9a129514760edb32a2 100644
---- a/net/minecraft/world/level/chunk/storage/RegionFile.java
-+++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
-@@ -28,7 +28,7 @@ import net.minecraft.nbt.NbtIo; // Paper
- import net.minecraft.world.level.ChunkPos;
- import org.slf4j.Logger;
-
--public class RegionFile implements AutoCloseable {
-+public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system
-
- private static final Logger LOGGER = LogUtils.getLogger();
- private static final int SECTOR_BYTES = 4096;
-@@ -52,6 +52,21 @@ public class RegionFile implements AutoCloseable {
- @VisibleForTesting
- protected final RegionBitmap usedSectors;
-
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final net.minecraft.nbt.CompoundTag data, final ChunkPos pos) throws IOException {
-+ final RegionFile.ChunkBuffer buffer = ((RegionFile)(Object)this).new ChunkBuffer(pos);
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer).moonrise$setWriteOnClose(false);
-+
-+ final DataOutputStream out = new DataOutputStream(this.version.wrap(buffer));
-+
-+ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData(
-+ data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE,
-+ out, ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer)buffer)::moonrise$write
-+ );
-+ }
-+ // Paper end - rewrite chunk system
-+
- public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException {
- this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format
- }
-@@ -224,6 +239,16 @@ public class RegionFile implements AutoCloseable {
-
- @Nullable
- private DataInputStream createExternalChunkInputStream(ChunkPos pos, byte flags) throws IOException {
-+ // Paper start - rewrite chunk system
-+ final DataInputStream is = this.createExternalChunkInputStream0(pos, flags);
-+ if (is == null) {
-+ return is;
-+ }
-+ return new ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker(is);
-+ }
-+ @Nullable
-+ private DataInputStream createExternalChunkInputStream0(ChunkPos pos, byte flags) throws IOException {
-+ // Paper end - rewrite chunk system
- Path path = this.getExternalChunkPath(pos);
-
- if (!Files.isRegularFile(path, new LinkOption[0])) {
-@@ -514,10 +539,29 @@ public class RegionFile implements AutoCloseable {
-
- }
- // Paper end
-- private class ChunkBuffer extends ByteArrayOutputStream {
-+ private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system
-
- private final ChunkPos pos;
-
-+ // Paper start - rewrite chunk system
-+ private boolean writeOnClose = true;
-+
-+ @Override
-+ public final boolean moonrise$getWriteOnClose() {
-+ return this.writeOnClose;
-+ }
-+
-+ @Override
-+ public final void moonrise$setWriteOnClose(final boolean value) {
-+ this.writeOnClose = value;
-+ }
-+
-+ @Override
-+ public final void moonrise$write(final RegionFile regionFile) throws IOException {
-+ regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count));
-+ }
-+ // Paper end - rewrite chunk system
-+
- public ChunkBuffer(final ChunkPos chunkcoordintpair) {
- super(8096);
- super.write(0);
-@@ -534,7 +578,7 @@ public class RegionFile implements AutoCloseable {
-
- JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i);
- bytebuffer.putInt(0, i);
-- RegionFile.this.write(this.pos, bytebuffer);
-+ if (this.writeOnClose) { RegionFile.this.write(this.pos, bytebuffer); } // Paper - rewrite chunk system
- }
- }
-
-diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index e6abe35d6c43b7f76cf3da129ec9552e7b82453e..fdf8e18d24442178b52397acb482ffa3306a32e3 100644
---- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -17,7 +17,7 @@ import net.minecraft.nbt.StreamTagVisitor;
- import net.minecraft.util.ExceptionCollector;
- import net.minecraft.world.level.ChunkPos;
-
--public final class RegionFileStorage implements AutoCloseable {
-+public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
-
- public static final String ANVIL_EXTENSION = ".mca";
- private static final int MAX_CACHE_SIZE = 256;
-@@ -26,33 +26,219 @@ public final class RegionFileStorage implements AutoCloseable {
- private final Path folder;
- private final boolean sync;
-
-- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
-+ // Paper start - rewrite chunk system
-+ private static final int REGION_SHIFT = 5;
-+ private static final int MAX_NON_EXISTING_CACHE = 1024 * 4;
-+ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet();
-+ private static String getRegionFileName(final int chunkX, final int chunkZ) {
-+ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca";
-+ }
-+
-+ private boolean doesRegionFilePossiblyExist(final long position) {
-+ synchronized (this.nonExistingRegionFiles) {
-+ if (this.nonExistingRegionFiles.contains(position)) {
-+ this.nonExistingRegionFiles.addAndMoveToFirst(position);
-+ return false;
-+ }
-+ return true;
-+ }
-+ }
-+
-+ private void createRegionFile(final long position) {
-+ synchronized (this.nonExistingRegionFiles) {
-+ this.nonExistingRegionFiles.remove(position);
-+ }
-+ }
-+
-+ private void markNonExisting(final long position) {
-+ synchronized (this.nonExistingRegionFiles) {
-+ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) {
-+ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) {
-+ this.nonExistingRegionFiles.removeLastLong();
-+ }
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ) {
-+ return !this.doesRegionFilePossiblyExist(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
-+ }
-+
-+ @Override
-+ public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) {
-+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
-+ }
-+
-+ @Override
-+ public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException {
-+ final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+
-+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
-+ if (ret != null) {
-+ return ret;
-+ }
-+
-+ if (!this.doesRegionFilePossiblyExist(key)) {
-+ return null;
-+ }
-+
-+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
-+ this.regionCache.removeLast().close();
-+ }
-+
-+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ));
-+
-+ if (!java.nio.file.Files.exists(regionPath)) {
-+ this.markNonExisting(key);
-+ return null;
-+ }
-+
-+ this.createRegionFile(key);
-+
-+ FileUtil.createDirectoriesSafe(this.folder);
-+
-+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
-+
-+ this.regionCache.putAndMoveToFirst(key, ret);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(
-+ final int chunkX, final int chunkZ, final CompoundTag compound
-+ ) throws IOException {
-+ if (compound == null) {
-+ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData(
-+ compound, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE,
-+ null, null
-+ );
-+ }
-+
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ final RegionFile regionFile = this.getRegionFile(pos);
-+
-+ // note: not required to keep regionfile loaded after this call, as the write param takes a regionfile as input
-+ // (and, the regionfile parameter is unused for writing until the write call)
-+ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData = ((ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile)regionFile).moonrise$startWrite(compound, pos);
-+
-+ try {
-+ NbtIo.write(compound, writeData.output());
-+ } finally {
-+ writeData.output().close();
-+ }
-+
-+ return writeData;
-+ }
-+
-+ @Override
-+ public final void moonrise$finishWrite(
-+ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData writeData
-+ ) throws IOException {
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ if (writeData.result() == ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE) {
-+ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ);
-+ if (regionFile != null) {
-+ regionFile.clear(pos);
-+ } // else: didn't exist
-+
-+ return;
-+ }
-+
-+ writeData.write().run(this.getRegionFile(pos));
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData(
-+ final int chunkX, final int chunkZ
-+ ) throws IOException {
-+ final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ);
-+
-+ final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ));
-+
-+ if (input == null) {
-+ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.NO_DATA, null, null
-+ );
-+ }
-+
-+ final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData ret = new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.HAS_DATA, input, null
-+ );
-+
-+ if (!(input instanceof ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker)) {
-+ // internal stream, which is fully read
-+ return ret;
-+ }
-+
-+ final CompoundTag syncRead = this.moonrise$finishRead(chunkX, chunkZ, ret);
-+
-+ if (syncRead == null) {
-+ // need to try again
-+ return this.moonrise$readData(chunkX, chunkZ);
-+ }
-+
-+ return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.SYNC_READ, null, syncRead
-+ );
-+ }
-+
-+ // if the return value is null, then the caller needs to re-try with a new call to readData()
-+ @Override
-+ public final CompoundTag moonrise$finishRead(
-+ final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData readData
-+ ) throws IOException {
-+ try {
-+ return NbtIo.read(readData.input());
-+ } finally {
-+ readData.input().close();
-+ }
-+ }
-+ // Paper end - rewrite chunk system
-+
-+ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
- this.folder = directory;
- this.sync = dsync;
- this.info = storageKey;
- }
-
-- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
-- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
-- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
-+ // Paper start - rewrite chunk system
-+ public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException {
-+ return this.getRegionFile(chunkcoordintpair, false);
-+ }
-+ // Paper end - rewrite chunk system
-
-- if (regionfile != null) {
-- return regionfile;
-- } else {
-- if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
-- ((RegionFile) this.regionCache.removeLast()).close();
-+ public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
-+ // Paper start - rewrite chunk system
-+ if (existingOnly) {
-+ return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z);
-+ }
-+ synchronized (this) {
-+ final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
-+
-+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
-+ if (ret != null) {
-+ return ret;
-+ }
-+
-+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
-+ this.regionCache.removeLast().close();
- }
-
-+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkcoordintpair.x, chunkcoordintpair.z));
-+
-+ this.createRegionFile(key);
-+
- FileUtil.createDirectoriesSafe(this.folder);
-- Path path = this.folder;
-- int j = chunkcoordintpair.getRegionX();
-- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
-- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
-- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
-
-- this.regionCache.putAndMoveToFirst(i, regionfile1);
-- return regionfile1;
-+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
-+
-+ this.regionCache.putAndMoveToFirst(key, ret);
-+
-+ return ret;
- }
-+ // Paper end - rewrite chunk system
- }
-
- // Paper start
-@@ -175,8 +361,14 @@ public final class RegionFileStorage implements AutoCloseable {
-
- }
-
-- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
-- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
-+ public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - rewrite chunk system - public
-+ RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system
-+ // Paper start - rewrite chunk system
-+ if (regionfile == null) {
-+ // if the RegionFile doesn't exist, no point in deleting from it
-+ return;
-+ }
-+ // Paper end - rewrite chunk system
-
- if (nbt == null) {
- regionfile.clear(pos);
-@@ -206,30 +398,37 @@ public final class RegionFileStorage implements AutoCloseable {
- }
-
- public void close() throws IOException {
-- ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
-- ObjectIterator objectiterator = this.regionCache.values().iterator();
--
-- while (objectiterator.hasNext()) {
-- RegionFile regionfile = (RegionFile) objectiterator.next();
--
-- try {
-- regionfile.close();
-- } catch (IOException ioexception) {
-- exceptionsuppressor.add(ioexception);
-+ // Paper start - rewrite chunk system
-+ synchronized (this) {
-+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
-+ for (final RegionFile regionFile : this.regionCache.values()) {
-+ try {
-+ regionFile.close();
-+ } catch (final IOException ex) {
-+ exceptionCollector.add(ex);
-+ }
- }
-- }
-
-- exceptionsuppressor.throwIfPresent();
-+ exceptionCollector.throwIfPresent();
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- public void flush() throws IOException {
-- ObjectIterator objectiterator = this.regionCache.values().iterator();
--
-- while (objectiterator.hasNext()) {
-- RegionFile regionfile = (RegionFile) objectiterator.next();
-+ // Paper start - rewrite chunk system
-+ synchronized (this) {
-+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
-+ for (final RegionFile regionFile : this.regionCache.values()) {
-+ try {
-+ regionFile.flush();
-+ } catch (final IOException ex) {
-+ exceptionCollector.add(ex);
-+ }
-+ }
-
-- regionfile.flush();
-+ exceptionCollector.throwIfPresent();
- }
-+ // Paper end - rewrite chunk system
-
- }
-
-diff --git a/net/minecraft/world/level/chunk/storage/SectionStorage.java b/net/minecraft/world/level/chunk/storage/SectionStorage.java
-index 75b2cf0e13c23a8348b7ff55e72e5ee755aa7460..c3beb7fcad46a917d2b61bd0a0e98e5106056728 100644
---- a/net/minecraft/world/level/chunk/storage/SectionStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/SectionStorage.java
-@@ -40,10 +40,10 @@ import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.LevelHeightAccessor;
- import org.slf4j.Logger;
-
--public class SectionStorage<R, P> implements AutoCloseable {
-+public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system
- static final Logger LOGGER = LogUtils.getLogger();
- private static final String SECTIONS_TAG = "Sections";
-- private final SimpleRegionStorage simpleRegionStorage;
-+ // Paper - rewrite chunk system
- private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>();
- private final LongLinkedOpenHashSet dirtyChunks = new LongLinkedOpenHashSet();
- private final Codec<P> codec;
-@@ -57,6 +57,18 @@ public class SectionStorage<R, P> implements AutoCloseable {
- private final Long2ObjectMap<CompletableFuture<Optional<SectionStorage.PackedChunk<P>>>> pendingLoads = new Long2ObjectOpenHashMap<>();
- private final Object loadLock = new Object();
-
-+ // Paper start - rewrite chunk system
-+ private final RegionFileStorage regionStorage;
-+
-+ @Override
-+ public final RegionFileStorage moonrise$getRegionStorage() {
-+ return this.regionStorage;
-+ }
-+
-+ @Override
-+ public void moonrise$close() throws IOException {}
-+ // Paper end - rewrite chunk system
-+
- public SectionStorage(
- SimpleRegionStorage storageAccess,
- Codec<P> codec,
-@@ -67,7 +79,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
- ChunkIOErrorReporter errorHandler,
- LevelHeightAccessor world
- ) {
-- this.simpleRegionStorage = storageAccess;
-+ // Paper - rewrite chunk system
- this.codec = codec;
- this.packer = serializer;
- this.unpacker = deserializer;
-@@ -75,6 +87,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
- this.registryAccess = registryManager;
- this.errorReporter = errorHandler;
- this.levelHeightAccessor = world;
-+ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system
- }
-
- protected void tick(BooleanSupplier shouldKeepTicking) {
-@@ -188,64 +201,15 @@ public class SectionStorage<R, P> implements AutoCloseable {
- }
-
- private CompletableFuture<Optional<SectionStorage.PackedChunk<P>>> tryRead(ChunkPos chunkPos) {
-- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE);
-- return this.simpleRegionStorage
-- .read(chunkPos)
-- .thenApplyAsync(
-- chunkNbt -> chunkNbt.map(
-- nbt -> SectionStorage.PackedChunk.parse(this.codec, registryOps, nbt, this.simpleRegionStorage, this.levelHeightAccessor)
-- ),
-- Util.backgroundExecutor().forName("parseSection")
-- )
-- .exceptionally(throwable -> {
-- if (throwable instanceof CompletionException) {
-- throwable = throwable.getCause();
-- }
--
-- if (throwable instanceof IOException iOException) {
-- LOGGER.error("Error reading chunk {} data from disk", chunkPos, iOException);
-- this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), chunkPos);
-- return Optional.empty();
-- } else {
-- throw new CompletionException(throwable);
-- }
-- });
-+ throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
- }
-
- private void unpackChunk(ChunkPos chunkPos, @Nullable SectionStorage.PackedChunk<P> result) {
-- if (result == null) {
-- for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) {
-- this.storage.put(getKey(chunkPos, i), Optional.empty());
-- }
-- } else {
-- boolean bl = result.versionChanged();
--
-- for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) {
-- long l = getKey(chunkPos, j);
-- Optional<R> optional = Optional.ofNullable(result.sectionsByY.get(j)).map(section -> this.unpacker.apply((P)section, () -> this.setDirty(l)));
-- this.storage.put(l, optional);
-- optional.ifPresent(object -> {
-- this.onSectionLoad(l);
-- if (bl) {
-- this.setDirty(l);
-- }
-- });
-- }
-- }
-+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
- }
-
- private void writeChunk(ChunkPos pos) {
-- RegistryOps<Tag> registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE);
-- Dynamic<Tag> dynamic = this.writeChunk(pos, registryOps);
-- Tag tag = dynamic.getValue();
-- if (tag instanceof CompoundTag) {
-- this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> {
-- this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos);
-- return null;
-- });
-- } else {
-- LOGGER.error("Expected compound tag, got {}", tag);
-- }
-+ throw new IllegalStateException("Only chunk system can write state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
- }
-
- private <T> Dynamic<T> writeChunk(ChunkPos chunkPos, DynamicOps<T> ops) {
-@@ -281,7 +245,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
- protected void onSectionLoad(long pos) {
- }
-
-- protected void setDirty(long pos) {
-+ public void setDirty(long pos) { // Paper - public
- Optional<R> optional = this.storage.get(pos);
- if (optional != null && !optional.isEmpty()) {
- this.dirtyChunks.add(ChunkPos.asLong(SectionPos.x(pos), SectionPos.z(pos)));
-@@ -302,7 +266,7 @@ public class SectionStorage<R, P> implements AutoCloseable {
-
- @Override
- public void close() throws IOException {
-- this.simpleRegionStorage.close();
-+ this.moonrise$close(); // Paper - rewrite chunk system
- }
-
- static record PackedChunk<T>(Int2ObjectMap<T> sectionsByY, boolean versionChanged) {
-diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-index 4bc7fa3324e9af3abce2acf960c7b0650aca2e36..0296f52fb2c871adbf2ce73a64d8f77fab826cd7 100644
---- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
-@@ -129,7 +129,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- long j = nbt.getLong("InhabitedTime");
- ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getString("Status"));
- UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY;
-- boolean flag = nbt.getBoolean("isLightOn");
-+ boolean flag = chunkstatus.isOrAfter(ChunkStatus.LIGHT) && (nbt.get("isLightOn") != null && nbt.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG) == ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION); // Paper - starlight
- DataResult dataresult;
- Logger logger;
- BlendingData.Packed blendingdata_d;
-@@ -246,7 +246,17 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- DataLayer nibblearray = nbttagcompound3.contains("BlockLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("BlockLight")) : null;
- DataLayer nibblearray1 = nbttagcompound3.contains("SkyLight", 7) ? new DataLayer(nbttagcompound3.getByteArray("SkyLight")) : null;
-
-- list4.add(new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1));
-+ // Paper start - starlight
-+ SerializableChunkData.SectionData serializableChunkData = new SerializableChunkData.SectionData(b0, chunksection, nibblearray, nibblearray1);
-+ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setBlockLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG));
-+ }
-+
-+ if (sectionData.contains(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, Tag.TAG_ANY_NUMERIC)) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)serializableChunkData).starlight$setSkyLightState(sectionData.getInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG));
-+ }
-+ list4.add(serializableChunkData);
-+ // Paper end - starlight
- }
-
- // CraftBukkit - ChunkBukkitValues
-@@ -254,6 +264,59 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- }
- }
-
-+ // Paper start - starlight
-+ private ProtoChunk loadStarlightLightData(final ServerLevel world, final ProtoChunk ret) {
-+
-+ final boolean hasSkyLight = world.dimensionType().hasSkyLight();
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world);
-+
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world);
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(world);
-+
-+ if (!this.lightCorrect) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles);
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles);
-+ return ret;
-+ }
-+
-+ try {
-+ for (final SerializableChunkData.SectionData sectionData : this.sectionData) {
-+ final int y = sectionData.y();
-+ final DataLayer blockLight = sectionData.blockLight();
-+ final DataLayer skyLight = sectionData.skyLight();
-+
-+ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState();
-+ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState();
-+
-+ if (blockState >= 0) {
-+ if (blockLight != null) {
-+ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(blockLight.getData()), blockState); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, blockState);
-+ }
-+ }
-+
-+ if (skyState >= 0 && hasSkyLight) {
-+ if (skyLight != null) {
-+ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(skyLight.getData()), skyState); // clone for data safety
-+ } else {
-+ skyNibbles[y - minSection] = new ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray(null, skyState);
-+ }
-+ }
-+ }
-+
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setBlockNibbles(blockNibbles);
-+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)ret).starlight$setSkyNibbles(skyNibbles);
-+ } catch (final Throwable thr) {
-+ ret.setLightCorrect(false);
-+
-+ LOGGER.error("Failed to parse light data for chunk " + ret.getPos() + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(world) + "'", thr);
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - starlight
-+
- public ProtoChunk read(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos expectedPos) {
- if (!Objects.equals(expectedPos, this.chunkPos)) {
- SerializableChunkData.LOGGER.error("Chunk file at {} is in the wrong location; relocating. (Expected {}, got {})", new Object[]{expectedPos, expectedPos, this.chunkPos});
-@@ -275,7 +338,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
-
- if (serializablechunkdata_b.chunkSection != null) {
- achunksection[world.getSectionIndexFromSectionY(serializablechunkdata_b.y)] = serializablechunkdata_b.chunkSection;
-- poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection);
-+ //poiStorage.checkConsistencyWithBlocks(sectionposition, serializablechunkdata_b.chunkSection); // Paper - rewrite chunk system
- }
-
- boolean flag2 = serializablechunkdata_b.blockLight != null;
-@@ -352,7 +415,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- }
-
- if (chunktype == ChunkType.LEVELCHUNK) {
-- return new ImposterProtoChunk((LevelChunk) object, false);
-+ return this.loadStarlightLightData(world, new ImposterProtoChunk((LevelChunk) object, false)); // Paper - starlight
- } else {
- ProtoChunk protochunk1 = (ProtoChunk) object;
- Iterator iterator2 = this.entities.iterator();
-@@ -382,7 +445,7 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- protochunk1.setCarvingMask(new CarvingMask(this.carvingMask, ((ChunkAccess) object).getMinY()));
- }
-
-- return protochunk1;
-+ return this.loadStarlightLightData(world, protochunk1); // Paper - starlight
- }
- }
-
-@@ -405,24 +468,48 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- throw new IllegalArgumentException("Chunk can't be serialized: " + String.valueOf(chunk));
- } else {
- ChunkPos chunkcoordintpair = chunk.getPos();
-- List<SerializableChunkData.SectionData> list = new ArrayList();
-+ List<SerializableChunkData.SectionData> list = new ArrayList(); final List<SerializableChunkData.SectionData> sections = list; // Paper - starlight - OBFHELPER
- LevelChunkSection[] achunksection = chunk.getSections();
- ThreadedLevelLightEngine lightenginethreaded = world.getChunkSource().getLightEngine();
-
-- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
-- int j = chunk.getSectionIndexFromSectionY(i);
-- boolean flag = j >= 0 && j < achunksection.length;
-- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-- DataLayer nibblearray2 = nibblearray != null && !nibblearray.isEmpty() ? nibblearray.copy() : null;
-- DataLayer nibblearray3 = nibblearray1 != null && !nibblearray1.isEmpty() ? nibblearray1.copy() : null;
-+ // Paper start - starlight
-+ final int minLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinLightSection(world);
-+ final int maxLightSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxLightSection(world);
-+ final int minBlockSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world);
-+
-+ final LevelChunkSection[] chunkSections = chunk.getSections();
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getBlockNibbles();
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles = ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)chunk).starlight$getSkyNibbles();
-+
-+ for (int lightSection = minLightSection; lightSection <= maxLightSection; ++lightSection) {
-+ final int lightSectionIdx = lightSection - minLightSection;
-+ final int blockSectionIdx = lightSection - minBlockSection;
-
-- if (flag || nibblearray2 != null || nibblearray3 != null) {
-- LevelChunkSection chunksection = flag ? achunksection[j].copy() : null;
-+ final LevelChunkSection chunkSection = (blockSectionIdx >= 0 && blockSectionIdx < chunkSections.length) ? chunkSections[blockSectionIdx].copy() : null;
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[lightSectionIdx].getSaveState();
-+ final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[lightSectionIdx].getSaveState();
-
-- list.add(new SerializableChunkData.SectionData(i, chunksection, nibblearray2, nibblearray3));
-+ if (chunkSection == null && blockNibble == null && skyNibble == null) {
-+ continue;
- }
-+
-+ final SerializableChunkData.SectionData sectionData = new SerializableChunkData.SectionData(
-+ lightSection, chunkSection,
-+ blockNibble == null ? null : (blockNibble.data == null ? null : new DataLayer(blockNibble.data)),
-+ skyNibble == null ? null : (skyNibble.data == null ? null : new DataLayer(skyNibble.data))
-+ );
-+
-+ if (blockNibble != null) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setBlockLightState(blockNibble.state);
-+ }
-+
-+ if (skyNibble != null) {
-+ ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$setSkyLightState(skyNibble.state);
-+ }
-+
-+ sections.add(sectionData);
- }
-+ // Paper end - starlight
-
- List<CompoundTag> list1 = new ArrayList(chunk.getBlockEntitiesPos().size());
- Iterator iterator = chunk.getBlockEntitiesPos().iterator();
-@@ -521,8 +608,8 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- Iterator iterator = this.sectionData.iterator();
-
- while (iterator.hasNext()) {
-- SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next();
-- CompoundTag nbttagcompound1 = new CompoundTag();
-+ SerializableChunkData.SectionData serializablechunkdata_b = (SerializableChunkData.SectionData) iterator.next(); final SerializableChunkData.SectionData sectionData = serializablechunkdata_b; // Paper - starlight - OBFHELPER
-+ CompoundTag nbttagcompound1 = new CompoundTag(); final CompoundTag sectionNBT = nbttagcompound1; // Paper - starlight - OBFHELPER
- LevelChunkSection chunksection = serializablechunkdata_b.chunkSection;
-
- if (chunksection != null) {
-@@ -538,6 +625,19 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- nbttagcompound1.putByteArray("SkyLight", serializablechunkdata_b.skyLight.getData());
- }
-
-+ // Paper start - starlight
-+ final int blockState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getBlockLightState();
-+ final int skyState = ((ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData)(Object)sectionData).starlight$getSkyLightState();
-+
-+ if (blockState > 0) {
-+ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.BLOCKLIGHT_STATE_TAG, blockState);
-+ }
-+
-+ if (skyState > 0) {
-+ sectionNBT.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.SKYLIGHT_STATE_TAG, skyState);
-+ }
-+ // Paper end - starlight
-+
- if (!nbttagcompound1.isEmpty()) {
- nbttagcompound1.putByte("Y", (byte) serializablechunkdata_b.y);
- nbttaglist.add(nbttagcompound1);
-@@ -577,6 +677,14 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- nbttagcompound.put("ChunkBukkitValues", this.persistentDataContainer);
- }
- // CraftBukkit end
-+ // Paper start - starlight
-+ if (this.lightCorrect && !this.chunkStatus.isBefore(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // clobber vanilla value to force vanilla to relight
-+ nbttagcompound.putBoolean("isLightOn", false);
-+ // store our light version
-+ nbttagcompound.putInt(ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_VERSION_TAG, ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.STARLIGHT_LIGHT_VERSION);
-+ }
-+ // Paper end - starlight
- return nbttagcompound;
- }
-
-@@ -763,7 +871,67 @@ public record SerializableChunkData(Registry<Biome> biomeRegistry, ChunkPos chun
- return nbttaglist;
- }
-
-- public static record SectionData(int y, @Nullable LevelChunkSection chunkSection, @Nullable DataLayer blockLight, @Nullable DataLayer skyLight) {
-+ // Paper start - starlight - convert from record
-+ public static final class SectionData implements ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData { // Paper - starlight - our diff
-+ private final int y;
-+ @javax.annotation.Nullable
-+ private final net.minecraft.world.level.chunk.LevelChunkSection chunkSection;
-+ @javax.annotation.Nullable
-+ private final net.minecraft.world.level.chunk.DataLayer blockLight;
-+ @javax.annotation.Nullable
-+ private final net.minecraft.world.level.chunk.DataLayer skyLight;
-+
-+ // Paper start - starlight - our diff
-+ private int blockLightState = -1;
-+ private int skyLightState = -1;
-+
-+ @Override
-+ public final int starlight$getBlockLightState() {
-+ return this.blockLightState;
-+ }
-+
-+ @Override
-+ public final void starlight$setBlockLightState(final int state) {
-+ this.blockLightState = state;
-+ }
-+
-+ @Override
-+ public final int starlight$getSkyLightState() {
-+ return this.skyLightState;
-+ }
-+
-+ @Override
-+ public final void starlight$setSkyLightState(final int state) {
-+ this.skyLightState = state;
-+ }
-+ // Paper end - starlight - our diff
-+
-+ public SectionData(int y, @javax.annotation.Nullable net.minecraft.world.level.chunk.LevelChunkSection chunkSection, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer blockLight, @javax.annotation.Nullable net.minecraft.world.level.chunk.DataLayer skyLight) {
-+ this.y = y;
-+ this.chunkSection = chunkSection;
-+ this.blockLight = blockLight;
-+ this.skyLight = skyLight;
-+ }
-+
-+ public int y() {
-+ return y;
-+ }
-+
-+ @javax.annotation.Nullable
-+ public net.minecraft.world.level.chunk.LevelChunkSection chunkSection() {
-+ return chunkSection;
-+ }
-+
-+ @javax.annotation.Nullable
-+ public net.minecraft.world.level.chunk.DataLayer blockLight() {
-+ return blockLight;
-+ }
-+
-+ @javax.annotation.Nullable
-+ public net.minecraft.world.level.chunk.DataLayer skyLight() {
-+ return skyLight;
-+ }
-+ // Paper end - starlight - convert from record
-
- }
-
-diff --git a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-index 578d270d5b7efb9ac8f5dde539170f6021e2b786..c5085ebf4e801837010f3750c5e89576bb0c27a5 100644
---- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-+++ b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
-@@ -14,7 +14,7 @@ import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.level.ChunkPos;
-
- public class SimpleRegionStorage implements AutoCloseable {
-- private final IOWorker worker;
-+ public final IOWorker worker; // Paper - public
- private final DataFixer fixerUpper;
- private final DataFixTypes dataFixType;
-
-diff --git a/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java
-index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644
---- a/net/minecraft/world/level/entity/EntityTickList.java
-+++ b/net/minecraft/world/level/entity/EntityTickList.java
-@@ -9,52 +9,38 @@ import javax.annotation.Nullable;
- import net.minecraft.world.entity.Entity;
-
- public class EntityTickList {
-- private Int2ObjectMap<Entity> active = new Int2ObjectLinkedOpenHashMap<>();
-- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>();
-- @Nullable
-- private Int2ObjectMap<Entity> iterated;
-+ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<net.minecraft.world.entity.Entity> entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system
-
- private void ensureActiveIsNotIterated() {
-- if (this.iterated == this.active) {
-- this.passive.clear();
--
-- for (Entry<Entity> entry : Int2ObjectMaps.fastIterable(this.active)) {
-- this.passive.put(entry.getIntKey(), entry.getValue());
-- }
--
-- Int2ObjectMap<Entity> int2ObjectMap = this.active;
-- this.active = this.passive;
-- this.passive = int2ObjectMap;
-- }
-+ // Paper - rewrite chunk system
- }
-
- public void add(Entity entity) {
- this.ensureActiveIsNotIterated();
-- this.active.put(entity.getId(), entity);
-+ this.entities.add(entity); // Paper - rewrite chunk system
- }
-
- public void remove(Entity entity) {
- this.ensureActiveIsNotIterated();
-- this.active.remove(entity.getId());
-+ this.entities.remove(entity); // Paper - rewrite chunk system
- }
-
- public boolean contains(Entity entity) {
-- return this.active.containsKey(entity.getId());
-+ return this.entities.contains(entity); // Paper - rewrite chunk system
- }
-
- public void forEach(Consumer<Entity> action) {
-- if (this.iterated != null) {
-- throw new UnsupportedOperationException("Only one concurrent iteration supported");
-- } else {
-- this.iterated = this.active;
--
-- try {
-- for (Entity entity : this.active.values()) {
-- action.accept(entity);
-- }
-- } finally {
-- this.iterated = null;
-+ // Paper start - rewrite chunk system
-+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
-+ // (by dfl iterator() is configured to not iterate over new entries)
-+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
-+ try {
-+ while (iterator.hasNext()) {
-+ action.accept(iterator.next());
- }
-+ } finally {
-+ iterator.finishedIterating();
- }
-+ // Paper end - rewrite chunk system
- }
- }
-diff --git a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-index 1fcc2b287ed723cf51720f80e68f18f4a15cf429..3f39d6c786d9dfdd9ad591e08ff05fcbb41a1df6 100644
---- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-+++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
-@@ -86,7 +86,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
- return CompletableFuture.supplyAsync(() -> {
- this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk);
- return chunk;
-- }, Util.backgroundExecutor().forName("init_biomes"));
-+ }, Runnable::run); // Paper - rewrite chunk system
- }
-
- private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) {
-@@ -311,7 +311,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
- }
-
- return ichunkaccess1;
-- }, Util.backgroundExecutor().forName("wgen_fill_noise"));
-+ }, Runnable::run); // Paper - rewrite chunk system
- }
-
- private ChunkAccess doFill(Blender blender, StructureManager structureAccessor, RandomState noiseConfig, ChunkAccess chunk, int minimumCellY, int cellHeight) {
-diff --git a/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-index c3586281c9594769593a6027ea0a78f7c76c0262..decdb275e83fa6244aa3a24458872b42c49d04ed 100644
---- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-+++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
-@@ -47,8 +47,13 @@ public class StructureCheck {
- private final BiomeSource biomeSource;
- private final long seed;
- private final DataFixer fixerUpper;
-- private final Long2ObjectMap<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>();
-- private final Map<Structure, Long2BooleanMap> featureChecks = new HashMap<>();
-+ // Paper start - rewrite chunk system
-+ // make sure to purge entries from the maps to prevent memory leaks
-+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
-+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
-+ private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<it.unimi.dsi.fastutil.objects.Object2IntMap<Structure>> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
-+ private final java.util.concurrent.ConcurrentHashMap<Structure, ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
-+ // Paper end - rewrite chunk system
-
- public StructureCheck(
- ChunkScanAccess chunkIoWorker,
-@@ -90,7 +95,7 @@ public class StructureCheck {
-
- public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
- long l = pos.toLong();
-- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
-+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system
- if (object2IntMap != null) {
- return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
- } else {
-@@ -100,9 +105,11 @@ public class StructureCheck {
- } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
- return StructureCheckResult.START_NOT_PRESENT;
- } else {
-- boolean bl = this.featureChecks
-- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap())
-- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type));
-+ // Paper start - rewrite chunk system
-+ boolean bl = this.featureChecksSafe
-+ .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
-+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type));
-+ // Paper end - rewrite chunk system
- return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
- }
- }
-@@ -228,15 +235,25 @@ public class StructureCheck {
- }
-
- private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) {
-- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure));
-- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos));
-+ // Paper start - rewrite chunk system
-+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure));
-+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything
-+ // from this map, as everything that checks this map uses loadedChunks first
-+ // so, one way or another it's a race condition that doesn't matter
-+ for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
-+ value.remove(pos);
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- public void incrementReference(ChunkPos pos, Structure structure) {
-- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> {
-- if (referencesByStructure == null || referencesByStructure.isEmpty()) {
-+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system
-+ if (referencesByStructure == null) {
- referencesByStructure = new Object2IntOpenHashMap<>();
-+ } else {
-+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
- }
-+ // Paper end - rewrite chunk system
-
- referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1);
- return referencesByStructure;
-diff --git a/net/minecraft/world/level/lighting/LevelLightEngine.java b/net/minecraft/world/level/lighting/LevelLightEngine.java
-index 8d90e783967280025d711c709facbcc87f611f8a..987e3397503cd07d3a2f172cede341297bc58dba 100644
---- a/net/minecraft/world/level/lighting/LevelLightEngine.java
-+++ b/net/minecraft/world/level/lighting/LevelLightEngine.java
-@@ -9,151 +9,111 @@ import net.minecraft.world.level.LightLayer;
- import net.minecraft.world.level.chunk.DataLayer;
- import net.minecraft.world.level.chunk.LightChunkGetter;
-
--public class LevelLightEngine implements LightEventListener {
-+public class LevelLightEngine implements LightEventListener, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system
- public static final int LIGHT_SECTION_PADDING = 1;
- public static final LevelLightEngine EMPTY = new LevelLightEngine();
- protected final LevelHeightAccessor levelHeightAccessor;
-- @Nullable
-- private final LightEngine<?, ?> blockEngine;
-- @Nullable
-- private final LightEngine<?, ?> skyEngine;
-+ // Paper start - rewrite chunk system
-+ protected final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface lightEngine;
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface starlight$getLightEngine() {
-+ return this.lightEngine;
-+ }
-+
-+ @Override
-+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
-+ final DataLayer nibble, final boolean trustEdges) {
-+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
-+ }
-+
-+ @Override
-+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos) {
-+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
-+ }
-+
-+ @Override
-+ public void starlight$clientChunkLoad(final ChunkPos pos, final net.minecraft.world.level.chunk.LevelChunk chunk) {
-+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
-+ }
-+ // Paper end - rewrite chunk system
-
- public LevelLightEngine(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) {
- this.levelHeightAccessor = chunkProvider.getLevel();
-- this.blockEngine = hasBlockLight ? new BlockLightEngine(chunkProvider) : null;
-- this.skyEngine = hasSkyLight ? new SkyLightEngine(chunkProvider) : null;
-+ // Paper start - rewrite chunk system
-+ if (chunkProvider.getLevel() instanceof net.minecraft.world.level.Level) {
-+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
-+ } else {
-+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
-+ }
-+ // Paper end - rewrite chunk system
- }
-
- private LevelLightEngine() {
- this.levelHeightAccessor = LevelHeightAccessor.create(0, 0);
-- this.blockEngine = null;
-- this.skyEngine = null;
-+ // Paper start - rewrite chunk system
-+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, false, false, (LevelLightEngine)(Object)this);
-+ // Paper end - rewrite chunk system
- }
-
- @Override
- public void checkBlock(BlockPos pos) {
-- if (this.blockEngine != null) {
-- this.blockEngine.checkBlock(pos);
-- }
--
-- if (this.skyEngine != null) {
-- this.skyEngine.checkBlock(pos);
-- }
-+ this.lightEngine.blockChange(pos.immutable()); // Paper - rewrite chunk system
- }
-
- @Override
- public boolean hasLightWork() {
-- return this.skyEngine != null && this.skyEngine.hasLightWork() || this.blockEngine != null && this.blockEngine.hasLightWork();
-+ return this.lightEngine.hasUpdates(); // Paper - rewrite chunk system
- }
-
- @Override
- public int runLightUpdates() {
-- int i = 0;
-- if (this.blockEngine != null) {
-- i += this.blockEngine.runLightUpdates();
-- }
--
-- if (this.skyEngine != null) {
-- i += this.skyEngine.runLightUpdates();
-- }
--
-- return i;
-+ final boolean hadUpdates = this.hasLightWork();
-+ this.lightEngine.propagateChanges();
-+ return hadUpdates ? 1 : 0; // Paper - rewrite chunk system
- }
-
- @Override
- public void updateSectionStatus(SectionPos pos, boolean notReady) {
-- if (this.blockEngine != null) {
-- this.blockEngine.updateSectionStatus(pos, notReady);
-- }
--
-- if (this.skyEngine != null) {
-- this.skyEngine.updateSectionStatus(pos, notReady);
-- }
-+ this.lightEngine.sectionChange(pos, notReady); // Paper - rewrite chunk system
- }
-
- @Override
- public void setLightEnabled(ChunkPos pos, boolean retainData) {
-- if (this.blockEngine != null) {
-- this.blockEngine.setLightEnabled(pos, retainData);
-- }
--
-- if (this.skyEngine != null) {
-- this.skyEngine.setLightEnabled(pos, retainData);
-- }
-+ // Paper - rewrite chunk system
- }
-
- @Override
- public void propagateLightSources(ChunkPos chunkPos) {
-- if (this.blockEngine != null) {
-- this.blockEngine.propagateLightSources(chunkPos);
-- }
--
-- if (this.skyEngine != null) {
-- this.skyEngine.propagateLightSources(chunkPos);
-- }
-+ // Paper - rewrite chunk system
- }
-
- public LayerLightEventListener getLayerListener(LightLayer lightType) {
-- if (lightType == LightLayer.BLOCK) {
-- return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine);
-- } else {
-- return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine);
-- }
-+ return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
- }
-
- public String getDebugData(LightLayer lightType, SectionPos pos) {
-- if (lightType == LightLayer.BLOCK) {
-- if (this.blockEngine != null) {
-- return this.blockEngine.getDebugData(pos.asLong());
-- }
-- } else if (this.skyEngine != null) {
-- return this.skyEngine.getDebugData(pos.asLong());
-- }
--
-- return "n/a";
-+ return "n/a"; // Paper - rewrite chunk system
- }
-
- public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightType, SectionPos pos) {
-- if (lightType == LightLayer.BLOCK) {
-- if (this.blockEngine != null) {
-- return this.blockEngine.getDebugSectionType(pos.asLong());
-- }
-- } else if (this.skyEngine != null) {
-- return this.skyEngine.getDebugSectionType(pos.asLong());
-- }
--
-- return LayerLightSectionStorage.SectionType.EMPTY;
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
- }
-
- public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
-- if (lightType == LightLayer.BLOCK) {
-- if (this.blockEngine != null) {
-- this.blockEngine.queueSectionData(pos.asLong(), nibbles);
-- }
-- } else if (this.skyEngine != null) {
-- this.skyEngine.queueSectionData(pos.asLong(), nibbles);
-- }
-+ // Paper - rewrite chunk system
- }
-
- public void retainData(ChunkPos pos, boolean retainData) {
-- if (this.blockEngine != null) {
-- this.blockEngine.retainData(pos, retainData);
-- }
--
-- if (this.skyEngine != null) {
-- this.skyEngine.retainData(pos, retainData);
-- }
-+ // Paper - rewrite chunk system
- }
-
- public int getRawBrightness(BlockPos pos, int ambientDarkness) {
-- int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(pos) - ambientDarkness;
-- int j = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(pos);
-- return Math.max(j, i);
-+ return this.lightEngine.getRawBrightness(pos, ambientDarkness); // Paper - rewrite chunk system
- }
-
- public boolean lightOnInColumn(long sectionPos) {
-- return this.blockEngine == null
-- || this.blockEngine.storage.lightOnInColumn(sectionPos) && (this.skyEngine == null || this.skyEngine.storage.lightOnInColumn(sectionPos));
-+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server
- }
-
- public int getLightSectionCount() {
-diff --git a/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java
-index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644
---- a/net/minecraft/world/level/material/FlowingFluid.java
-+++ b/net/minecraft/world/level/material/FlowingFluid.java
-@@ -55,6 +55,48 @@ public abstract class FlowingFluid extends Fluid {
- });
- private final Map<FluidState, VoxelShape> shapes = Maps.newIdentityHashMap();
-
-+ // Paper start - fluid method optimisations
-+ private FluidState sourceFalling;
-+ private FluidState sourceNotFalling;
-+
-+ private static final int TOTAL_FLOWING_STATES = FALLING.getPossibleValues().size() * LEVEL.getPossibleValues().size();
-+ private static final int MIN_LEVEL = LEVEL.getPossibleValues().stream().sorted().findFirst().get().intValue();
-+
-+ // index = (falling ? 1 : 0) + level*2
-+ private FluidState[] flowingLookUp;
-+ private volatile boolean init;
-+
-+ private static final int COLLISION_OCCLUSION_CACHE_SIZE = 2048;
-+ private static final ThreadLocal<ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[]> COLLISION_OCCLUSION_CACHE = ThreadLocal.withInitial(() -> new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[COLLISION_OCCLUSION_CACHE_SIZE]);
-+
-+
-+ /**
-+ * Due to init order, we need to use callbacks to initialise our state
-+ */
-+ private void init() {
-+ synchronized (this) {
-+ if (this.init) {
-+ return;
-+ }
-+ this.flowingLookUp = new FluidState[TOTAL_FLOWING_STATES];
-+ final FluidState defaultFlowState = this.getFlowing().defaultFluidState();
-+ for (int i = 0; i < TOTAL_FLOWING_STATES; ++i) {
-+ final int falling = i & 1;
-+ final int level = (i >>> 1) + MIN_LEVEL;
-+
-+ this.flowingLookUp[i] = defaultFlowState.setValue(FALLING, falling == 1 ? Boolean.TRUE : Boolean.FALSE)
-+ .setValue(LEVEL, Integer.valueOf(level));
-+ }
-+
-+ final FluidState defaultFallState = this.getSource().defaultFluidState();
-+ this.sourceFalling = defaultFallState.setValue(FALLING, Boolean.TRUE);
-+ this.sourceNotFalling = defaultFallState.setValue(FALLING, Boolean.FALSE);
-+
-+ this.init = true;
-+ }
-+ }
-+ // Paper end - fluid method optimisations
-+
- public FlowingFluid() {}
-
- @Override
-@@ -246,65 +288,70 @@ public abstract class FlowingFluid extends Fluid {
- }
- }
-
-- private static boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) {
-- VoxelShape voxelshape = fromState.getCollisionShape(world, fromPos);
-+ // Paper start - fluid method optimisations
-+ private static boolean canPassThroughWall(final Direction direction, final BlockGetter level,
-+ final BlockPos fromPos, final BlockState fromState,
-+ final BlockPos toPos, final BlockState toState) {
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$emptyCollisionShape() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$emptyCollisionShape()) {
-+ // don't even try to cache simple cases
-+ return true;
-+ }
-
-- if (voxelshape == Shapes.block()) {
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$occludesFullBlock() | ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$occludesFullBlock()) {
-+ // don't even try to cache simple cases
- return false;
-- } else {
-- VoxelShape voxelshape1 = state.getCollisionShape(world, pos);
--
-- if (voxelshape1 == Shapes.block()) {
-- return false;
-- } else if (voxelshape1 == Shapes.empty() && voxelshape == Shapes.empty()) {
-- return true;
-- } else {
-- Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
--
-- if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) {
-- object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get();
-- } else {
-- object2bytelinkedopenhashmap = null;
-- }
-+ }
-
-- FlowingFluid.BlockStatePairKey fluidtypeflowing_a;
-+ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey[] cache = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$hasCache() & ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$hasCache() ?
-+ COLLISION_OCCLUSION_CACHE.get() : null;
-
-- if (object2bytelinkedopenhashmap != null) {
-- fluidtypeflowing_a = new FlowingFluid.BlockStatePairKey(state, fromState, face);
-- byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(fluidtypeflowing_a);
-+ final int keyIndex
-+ = (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)fromState).moonrise$uniqueId1() ^ ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)toState).moonrise$uniqueId2() ^ ((ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection)(Object)direction).moonrise$uniqueId())
-+ & (COLLISION_OCCLUSION_CACHE_SIZE - 1);
-
-- if (b0 != 127) {
-- return b0 != 0;
-- }
-- } else {
-- fluidtypeflowing_a = null;
-- }
--
-- boolean flag = !Shapes.mergedFaceOccludes(voxelshape1, voxelshape, face);
-+ if (cache != null) {
-+ final ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey cached = cache[keyIndex];
-+ if (cached != null && cached.first() == fromState && cached.second() == toState && cached.direction() == direction) {
-+ return cached.result();
-+ }
-+ }
-
-- if (object2bytelinkedopenhashmap != null) {
-- if (object2bytelinkedopenhashmap.size() == 200) {
-- object2bytelinkedopenhashmap.removeLastByte();
-- }
-+ final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos);
-+ final VoxelShape shape2 = toState.getCollisionShape(level, toPos);
-
-- object2bytelinkedopenhashmap.putAndMoveToFirst(fluidtypeflowing_a, (byte) (flag ? 1 : 0));
-- }
-+ final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction);
-
-- return flag;
-- }
-+ if (cache != null) {
-+ // we can afford to replace in-use keys more often due to the excessive caching the collision patch does in mergedFaceOccludes
-+ cache[keyIndex] = new ca.spottedleaf.moonrise.patches.collisions.util.FluidOcclusionCacheKey(fromState, toState, direction, result);
- }
-+
-+ return result;
- }
-+ // Paper end - fluid method optimisations
-
- public abstract Fluid getFlowing();
-
- public FluidState getFlowing(int level, boolean falling) {
-- return (FluidState) ((FluidState) this.getFlowing().defaultFluidState().setValue(FlowingFluid.LEVEL, level)).setValue(FlowingFluid.FALLING, falling);
-+ // Paper start - fluid method optimisations
-+ final int amount = level;
-+ if (!this.init) {
-+ this.init();
-+ }
-+ final int index = (falling ? 1 : 0) | ((amount - MIN_LEVEL) << 1);
-+ return this.flowingLookUp[index];
-+ // Paper end - fluid method optimisations
- }
-
- public abstract Fluid getSource();
-
- public FluidState getSource(boolean falling) {
-- return (FluidState) this.getSource().defaultFluidState().setValue(FlowingFluid.FALLING, falling);
-+ // Paper start - fluid method optimisations
-+ if (!this.init) {
-+ this.init();
-+ }
-+ return falling ? this.sourceFalling : this.sourceNotFalling;
-+ // Paper end - fluid method optimisations
- }
-
- protected abstract boolean canConvertToSource(ServerLevel world);
-diff --git a/net/minecraft/world/level/material/FluidState.java b/net/minecraft/world/level/material/FluidState.java
-index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644
---- a/net/minecraft/world/level/material/FluidState.java
-+++ b/net/minecraft/world/level/material/FluidState.java
-@@ -22,12 +22,30 @@ import net.minecraft.world.level.block.state.properties.Property;
- import net.minecraft.world.phys.Vec3;
- import net.minecraft.world.phys.shapes.VoxelShape;
-
--public final class FluidState extends StateHolder<Fluid, FluidState> {
-+public final class FluidState extends StateHolder<Fluid, FluidState> implements ca.spottedleaf.moonrise.patches.fluid.FluidFluidState { // Paper - fluid method optimisations
- public static final Codec<FluidState> CODEC = codec(BuiltInRegistries.FLUID.byNameCodec(), Fluid::defaultFluidState).stable();
- public static final int AMOUNT_MAX = 9;
- public static final int AMOUNT_FULL = 8;
- protected final boolean isEmpty; // Paper - Perf: moved from isEmpty()
-
-+ // Paper start - fluid method optimisations
-+ private int amount;
-+ //private boolean isEmpty;
-+ private boolean isSource;
-+ private float ownHeight;
-+ private boolean isRandomlyTicking;
-+ private BlockState legacyBlock;
-+
-+ @Override
-+ public final void moonrise$initCaches() {
-+ this.amount = this.getType().getAmount((FluidState)(Object)this);
-+ //this.isEmpty = this.getType().isEmpty();
-+ this.isSource = this.getType().isSource((FluidState)(Object)this);
-+ this.ownHeight = this.getType().getOwnHeight((FluidState)(Object)this);
-+ this.isRandomlyTicking = this.getType().isRandomlyTicking();
-+ }
-+ // Paper end - fluid method optimisations
-+
- public FluidState(Fluid fluid, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<FluidState> codec) {
- super(fluid, propertyMap, codec);
- this.isEmpty = fluid.isEmpty(); // Paper - Perf: moved from isEmpty()
-@@ -38,11 +56,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
- }
-
- public boolean isSource() {
-- return this.getType().isSource(this);
-+ return this.isSource; // Paper - fluid method optimisations
- }
-
- public boolean isSourceOfType(Fluid fluid) {
-- return this.owner == fluid && this.owner.isSource(this);
-+ return this.isSource && this.owner == fluid; // Paper - fluid method optimisations
- }
-
- public boolean isEmpty() {
-@@ -54,11 +72,11 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
- }
-
- public float getOwnHeight() {
-- return this.getType().getOwnHeight(this);
-+ return this.ownHeight; // Paper - fluid method optimisations
- }
-
- public int getAmount() {
-- return this.getType().getAmount(this);
-+ return this.amount; // Paper - fluid method optimisations
- }
-
- public boolean shouldRenderBackwardUpFace(BlockGetter world, BlockPos pos) {
-@@ -84,7 +102,7 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
- }
-
- public boolean isRandomlyTicking() {
-- return this.getType().isRandomlyTicking();
-+ return this.isRandomlyTicking; // Paper - fluid method optimisations
- }
-
- public void randomTick(ServerLevel world, BlockPos pos, RandomSource random) {
-@@ -96,7 +114,12 @@ public final class FluidState extends StateHolder<Fluid, FluidState> {
- }
-
- public BlockState createLegacyBlock() {
-- return this.getType().createLegacyBlock(this);
-+ // Paper start - fluid method optimisations
-+ if (this.legacyBlock != null) {
-+ return this.legacyBlock;
-+ }
-+ return this.legacyBlock = this.getType().createLegacyBlock((FluidState)(Object)this);
-+ // Paper end - fluid method optimisations
- }
-
- @Nullable
-diff --git a/net/minecraft/world/phys/AABB.java b/net/minecraft/world/phys/AABB.java
-index 5dc2674b537f4a61b2e21a21bdb2e8dc090d3a3c..6cf6d4ec7b9e43c7b2b4c0e2fb080964ff588130 100644
---- a/net/minecraft/world/phys/AABB.java
-+++ b/net/minecraft/world/phys/AABB.java
-@@ -331,7 +331,7 @@ public class AABB {
- }
-
- @Nullable
-- private static Direction getDirection(
-+ public static Direction getDirection( // Paper - optimise collisions - public
- AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ
- ) {
- return getDirection(
-diff --git a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-index 4fee67f7214b464b9e09862778e3ef187fcb8b72..31a54af04ab072a433d6df9fe37beb12243fea80 100644
---- a/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-+++ b/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape {
- );
- }
-
-- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
-+ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public
- super(shape);
- int i = shape.getXSize() + 1;
- int j = shape.getYSize() + 1;
-@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape {
- new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")
- );
- }
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
- }
-
- @Override
-diff --git a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..97ef481156ec5d821779f126ab98a8f28cbaf30b 100644
---- a/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-+++ b/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-@@ -4,13 +4,13 @@ import java.util.BitSet;
- import net.minecraft.core.Direction;
-
- public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
-- private final BitSet storage;
-- private int xMin;
-- private int yMin;
-- private int zMin;
-- private int xMax;
-- private int yMax;
-- private int zMax;
-+ public final BitSet storage; // Paper - optimise collisions - public
-+ public int xMin; // Paper - optimise collisions - public
-+ public int yMin; // Paper - optimise collisions - public
-+ public int zMin; // Paper - optimise collisions - public
-+ public int xMax; // Paper - optimise collisions - public
-+ public int yMax; // Paper - optimise collisions - public
-+ public int zMax; // Paper - optimise collisions - public
-
- public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
- super(sizeX, sizeY, sizeZ);
-@@ -150,47 +150,109 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
- return bitSetDiscreteVoxelShape;
- }
-
-- protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) {
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet);
-+ // Paper start - optimise collisions
-+ public static void forAllBoxes(final DiscreteVoxelShape shape, final DiscreteVoxelShape.IntLineConsumer consumer, final boolean mergeAdjacent) {
-+ // Paper - remove debug
-+ // called with the shape of a VoxelShape, so we can expect the cache to exist
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cache = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape) shape).moonrise$getOrCreateCachedShapeData();
-+
-+ final int sizeX = cache.sizeX();
-+ final int sizeY = cache.sizeY();
-+ final int sizeZ = cache.sizeZ();
-+
-+ int indexX;
-+ int indexY = 0;
-+ int indexZ;
-+
-+ int incY = sizeZ;
-+ int incX = sizeZ * sizeY;
-+
-+ long[] bitset = cache.voxelSet();
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ if (!mergeAdjacent) {
-+ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply
-+ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid
-+ // the multiplication
-+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
-+ indexX = indexY;
-+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
-+ indexZ = indexX;
-+ for (int z = 0; z < sizeZ; ++z, ++indexZ) {
-+ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) {
-+ consumer.consume(x, y, z, x + 1, y + 1, z + 1);
-+ }
-+ }
-+ }
-+ }
-+ } else {
-+ // same notes about loop order as the above
-+ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize())
-
-- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) {
-- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) {
-- int k = -1;
-+ // only clone when we may write to it
-+ bitset = ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(bitset);
-
-- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) {
-- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) {
-- if (coalesce) {
-- if (k == -1) {
-- k = l;
-- }
-- } else {
-- callback.consume(j, i, l, j + 1, i + 1, l + 1);
-+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
-+ indexX = indexY;
-+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
-+ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex; ) {
-+ final int firstSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex);
-+
-+ if (firstSetZ == -1) {
-+ break;
-+ }
-+
-+ int lastSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex);
-+ if (lastSetZ == -1) {
-+ lastSetZ = endIndex;
- }
-- } else if (k != -1) {
-- int m = j;
-- int n = i;
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i);
--
-- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) {
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i);
-- m++;
-+
-+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ);
-+
-+ // try to merge neighbouring on the X axis
-+ int endX = x + 1; // exclusive
-+ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX;
-+ endX < sizeX && ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd);
-+ neighbourIdxStart += incX, neighbourIdxEnd += incX) {
-+
-+ ++endX;
-+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd);
- }
-
-- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
-- for (int o = j; o <= m; o++) {
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
-+ // try to merge neighbouring on the Y axis
-+
-+ int endY; // exclusive
-+ int firstSetZY, lastSetZY;
-+ y_merge:
-+ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY;
-+ firstSetZY += incY, lastSetZY += incY) {
-+
-+ // test the whole XZ range
-+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
-+ ++testX, start += incX, end += incX) {
-+ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, start, end)) {
-+ break y_merge;
-+ }
- }
-
-- n++;
-+ ++endY;
-+
-+ // passed, so we can clear it
-+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
-+ ++testX, start += incX, end += incX) {
-+ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, start, end);
-+ }
- }
-
-- callback.consume(j, i, k, m + 1, n + 1, l);
-- k = -1;
-+ consumer.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX);
-+ zIdx = lastSetZ;
- }
- }
- }
- }
- }
-+ // Paper end - optimise collisions
-
- private boolean isZStripFull(int z1, int z2, int x, int y) {
- return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, z1)) >= this.getIndex(x, y, z2);
-diff --git a/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7bbe0f4b38 100644
---- a/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-+++ b/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-@@ -7,6 +7,7 @@ import net.minecraft.util.Mth;
- public final class CubeVoxelShape extends VoxelShape {
- protected CubeVoxelShape(DiscreteVoxelShape voxels) {
- super(voxels);
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
- }
-
- @Override
-diff --git a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-index 01693ba050b12b9debcdaefceeff9cbcd503b369..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644
---- a/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-+++ b/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-@@ -3,12 +3,79 @@ package net.minecraft.world.phys.shapes;
- import net.minecraft.core.AxisCycle;
- import net.minecraft.core.Direction;
-
--public abstract class DiscreteVoxelShape {
-+public abstract class DiscreteVoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape { // Paper - optimise collisions
- private static final Direction.Axis[] AXIS_VALUES = Direction.Axis.values();
- protected final int xSize;
- protected final int ySize;
- protected final int zSize;
-
-+ // Paper start - optimise collisions
-+ // ignore race conditions on field read/write: the shape is static, so it doesn't matter
-+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData;
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData() {
-+ if (this.cachedShapeData != null) {
-+ return this.cachedShapeData;
-+ }
-+
-+ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this;
-+
-+ final int sizeX = discreteVoxelShape.getXSize();
-+ final int sizeY = discreteVoxelShape.getYSize();
-+ final int sizeZ = discreteVoxelShape.getZSize();
-+
-+ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive
-+
-+ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6;
-+ long[] voxelSet;
-+
-+ final boolean isEmpty = discreteVoxelShape.isEmpty();
-+
-+ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) {
-+ voxelSet = bitsetShape.storage.toLongArray();
-+ if (voxelSet.length < longsRequired) {
-+ // happens when the later long values are 0L, so we need to resize
-+ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired);
-+ }
-+ } else {
-+ voxelSet = new long[longsRequired];
-+ if (!isEmpty) {
-+ final int mulX = sizeZ * sizeY;
-+ for (int x = 0; x < sizeX; ++x) {
-+ for (int y = 0; y < sizeY; ++y) {
-+ for (int z = 0; z < sizeZ; ++z) {
-+ if (discreteVoxelShape.isFull(x, y, z)) {
-+ // index = z + y*size_z + x*(size_z*size_y)
-+ final int index = z + y * sizeZ + x * mulX;
-+
-+ voxelSet[index >>> 6] |= 1L << index;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && (voxelSet[0] & 1L) != 0L;
-+
-+ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X);
-+ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y);
-+ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z);
-+
-+ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X);
-+ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y);
-+ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z);
-+
-+ return this.cachedShapeData = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData(
-+ sizeX, sizeY, sizeZ, voxelSet,
-+ minFullX, minFullY, minFullZ,
-+ maxFullX, maxFullY, maxFullZ,
-+ isEmpty, hasSingleAABB
-+ );
-+ }
-+ // Paper end - optimise collisions
-+
- protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
- if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) {
- this.xSize = sizeX;
-diff --git a/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644
---- a/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-+++ b/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
- import it.unimi.dsi.fastutil.doubles.DoubleList;
-
- public class OffsetDoubleList extends AbstractDoubleList {
-- private final DoubleList delegate;
-- private final double offset;
-+ public final DoubleList delegate; // Paper - optimise collisions - public
-+ public final double offset; // Paper - optimise collisions - public
-
- public OffsetDoubleList(DoubleList oldList, double offset) {
- this.delegate = oldList;
-diff --git a/net/minecraft/world/phys/shapes/Shapes.java b/net/minecraft/world/phys/shapes/Shapes.java
-index 76d7435e6fe81a3f1d24b35eae72d06232a1792b..ca3a2419252721bb3b3b719eb19afb5f175394c0 100644
---- a/net/minecraft/world/phys/shapes/Shapes.java
-+++ b/net/minecraft/world/phys/shapes/Shapes.java
-@@ -16,9 +16,15 @@ public final class Shapes {
- public static final double EPSILON = 1.0E-7;
- public static final double BIG_EPSILON = 1.0E-6;
- private static final VoxelShape BLOCK = Util.make(() -> {
-- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1);
-- discreteVoxelShape.fill(0, 0, 0);
-- return new CubeVoxelShape(discreteVoxelShape);
-+ // Paper start - optimise collisions
-+ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1);
-+ shape.fill(0, 0, 0);
-+
-+ return new ArrayVoxelShape(
-+ shape,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE
-+ );
-+ // Paper end - optimise collisions
- });
- public static final VoxelShape INFINITY = box(
- Double.NEGATIVE_INFINITY,
-@@ -43,6 +49,30 @@ public final class Shapes {
- return BLOCK;
- }
-
-+ // Paper start - optimise collisions
-+ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] {
-+ DoubleArrayList.wrap(generateCubeParts(1 << 0)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 1)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 2)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 3))
-+ };
-+
-+ private static double[] generateCubeParts(final int parts) {
-+ // note: parts is a power of two, so we do not need to worry about loss of precision here
-+ // note: parts is from [2^0, 2^3]
-+ final double inc = 1.0 / (double)parts;
-+
-+ final double[] ret = new double[parts + 1];
-+ double val = 0.0;
-+ for (int i = 0; i <= parts; ++i) {
-+ ret[i] = val;
-+ val += inc;
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - optimise collisions
-+
- public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
- if (!(minX > maxX) && !(minY > maxY) && !(minZ > maxZ)) {
- return create(minX, minY, minZ, maxX, maxY, maxZ);
-@@ -52,39 +82,42 @@ public final class Shapes {
- }
-
- public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
-+ // Paper start - optimise collisions
- if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) {
-- int i = findBits(minX, maxX);
-- int j = findBits(minY, maxY);
-- int k = findBits(minZ, maxZ);
-- if (i < 0 || j < 0 || k < 0) {
-+ final int bitsX = findBits(minX, maxX);
-+ final int bitsY = findBits(minY, maxY);
-+ final int bitsZ = findBits(minZ, maxZ);
-+ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) {
-+ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) {
-+ return BLOCK;
-+ } else {
-+ final int sizeX = 1 << bitsX;
-+ final int sizeY = 1 << bitsY;
-+ final int sizeZ = 1 << bitsZ;
-+ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds(
-+ sizeX, sizeY, sizeZ,
-+ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ),
-+ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ)
-+ );
-+ return new ArrayVoxelShape(
-+ shape,
-+ PARTS_BY_BITS[bitsX],
-+ PARTS_BY_BITS[bitsY],
-+ PARTS_BY_BITS[bitsZ]
-+ );
-+ }
-+ } else {
- return new ArrayVoxelShape(
- BLOCK.shape,
-- DoubleArrayList.wrap(new double[]{minX, maxX}),
-- DoubleArrayList.wrap(new double[]{minY, maxY}),
-- DoubleArrayList.wrap(new double[]{minZ, maxZ})
-+ minX == 0.0 && maxX == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }),
-+ minY == 0.0 && maxY == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }),
-+ minZ == 0.0 && maxZ == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ })
- );
-- } else if (i == 0 && j == 0 && k == 0) {
-- return block();
-- } else {
-- int l = 1 << i;
-- int m = 1 << j;
-- int n = 1 << k;
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(
-- l,
-- m,
-- n,
-- (int)Math.round(minX * (double)l),
-- (int)Math.round(minY * (double)m),
-- (int)Math.round(minZ * (double)n),
-- (int)Math.round(maxX * (double)l),
-- (int)Math.round(maxY * (double)m),
-- (int)Math.round(maxZ * (double)n)
-- );
-- return new CubeVoxelShape(bitSetDiscreteVoxelShape);
- }
- } else {
-- return empty();
-+ return EMPTY;
- }
-+ // Paper end - optimise collisions
- }
-
- public static VoxelShape create(AABB box) {
-@@ -119,80 +152,54 @@ public final class Shapes {
- return join(first, second, BooleanOp.OR);
- }
-
-- public static VoxelShape or(VoxelShape first, VoxelShape... others) {
-- return Arrays.stream(others).reduce(first, Shapes::or);
-+ // Paper start - optimise collisions
-+ public static VoxelShape or(VoxelShape shape, VoxelShape... others) {
-+ int size = others.length;
-+ if (size == 0) {
-+ return shape;
-+ }
-+
-+ // reduce complexity of joins by splitting the merges
-+
-+ // add extra slot for first shape
-+ ++size;
-+ final VoxelShape[] tmp = Arrays.copyOf(others, size);
-+ // insert first shape
-+ tmp[size - 1] = shape;
-+
-+ while (size > 1) {
-+ int newSize = 0;
-+ for (int i = 0; i < size; i += 2) {
-+ final int next = i + 1;
-+ if (next >= size) {
-+ // nothing to merge with, so leave it for next iteration
-+ tmp[newSize++] = tmp[i];
-+ break;
-+ } else {
-+ // merge with adjacent
-+ final VoxelShape first = tmp[i];
-+ final VoxelShape second = tmp[next];
-+
-+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
-+ }
-+ }
-+ size = newSize;
-+ }
-+
-+ return tmp[0].optimize();
-+ // Paper end - optimise collisions
- }
-
- public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) {
-- return joinUnoptimized(first, second, function).optimize();
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions
- }
-
- public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) {
-- if (function.apply(false, false)) {
-- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
-- } else if (one == two) {
-- return function.apply(true, true) ? one : empty();
-- } else {
-- boolean bl = function.apply(true, false);
-- boolean bl2 = function.apply(false, true);
-- if (one.isEmpty()) {
-- return bl2 ? two : empty();
-- } else if (two.isEmpty()) {
-- return bl ? one : empty();
-- } else {
-- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2);
-- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2);
-- IndexMerger indexMerger3 = createIndexMerger(
-- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2
-- );
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(
-- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function
-- );
-- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger
-- && indexMerger2 instanceof DiscreteCubeMerger
-- && indexMerger3 instanceof DiscreteCubeMerger
-- ? new CubeVoxelShape(bitSetDiscreteVoxelShape)
-- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList()));
-- }
-- }
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions
- }
-
- public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
-- if (predicate.apply(false, false)) {
-- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
-- } else {
-- boolean bl = shape1.isEmpty();
-- boolean bl2 = shape2.isEmpty();
-- if (!bl && !bl2) {
-- if (shape1 == shape2) {
-- return predicate.apply(true, true);
-- } else {
-- boolean bl3 = predicate.apply(true, false);
-- boolean bl4 = predicate.apply(false, true);
--
-- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) {
-- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) {
-- return bl3 || bl4;
-- }
--
-- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) {
-- return bl3 || bl4;
-- }
-- }
--
-- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4);
-- IndexMerger indexMerger2 = createIndexMerger(
-- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4
-- );
-- IndexMerger indexMerger3 = createIndexMerger(
-- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4
-- );
-- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate);
-- }
-- } else {
-- return predicate.apply(!bl, !bl2);
-- }
-- }
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions
- }
-
- private static boolean joinIsNotEmpty(
-@@ -219,51 +226,116 @@ public final class Shapes {
- return maxDist;
- }
-
-- public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) {
-- if (shape == block() && neighbor == block()) {
-+ // Paper start - optimise collisions
-+ public static boolean blockOccudes(final VoxelShape first, final VoxelShape second, final Direction direction) {
-+ final boolean firstBlock = first == BLOCK;
-+ final boolean secondBlock = second == BLOCK;
-+
-+ if (firstBlock & secondBlock) {
- return true;
-- } else if (neighbor.isEmpty()) {
-+ }
-+
-+ if (first.isEmpty() | second.isEmpty()) {
-+ return false;
-+ }
-+
-+ // we optimise getOpposite, so we can use it
-+ // secondly, use our cache to retrieve sliced shape
-+ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction);
-+ if (newFirst.isEmpty()) {
- return false;
-- } else {
-- Direction.Axis axis = direction.getAxis();
-- Direction.AxisDirection axisDirection = direction.getAxisDirection();
-- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor;
-- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape;
-- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND;
-- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)
-- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)
-- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
- }
-+ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite());
-+ if (newSecond.isEmpty()) {
-+ return false;
-+ }
-+
-+ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST);
-+ // Paper end - optimise collisions
- }
-
-- public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) {
-- if (one != block() && two != block()) {
-- Direction.Axis axis = direction.getAxis();
-- Direction.AxisDirection axisDirection = direction.getAxisDirection();
-- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two;
-- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one;
-- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) {
-- voxelShape = empty();
-- }
-+ // Paper start - optimise collisions
-+ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) {
-+ // if the combined bounds of the two shapes cannot occlude, then neither can the merged
-+ final AABB bounds1 = shape1.bounds();
-+ final AABB bounds2 = shape2.bounds();
-
-- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) {
-- voxelShape2 = empty();
-- }
-+ final double minX = Math.min(bounds1.minX, bounds2.minX);
-+ final double minY = Math.min(bounds1.minY, bounds2.minY);
-+ final double minZ = Math.min(bounds1.minZ, bounds2.minZ);
-
-- return !joinIsNotEmpty(
-- block(),
-- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR),
-- BooleanOp.ONLY_FIRST
-- );
-- } else {
-+ final double maxX = Math.max(bounds1.maxX, bounds2.maxX);
-+ final double maxY = Math.max(bounds1.maxY, bounds2.maxY);
-+ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ);
-+
-+ return (minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
-+ (minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
-+ (minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON));
-+ }
-+ // Paper end - optimise collisions
-+
-+ // Paper start - optimise collisions
-+ public static boolean mergedFaceOccludes(final VoxelShape first, final VoxelShape second, final Direction direction) {
-+ // see if any of the shapes on their own occludes, only if cached
-+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$occludesFullBlockIfCached()) {
-+ return true;
-+ }
-+
-+ if (first.isEmpty() & second.isEmpty()) {
-+ return false;
-+ }
-+
-+ // we optimise getOpposite, so we can use it
-+ // secondly, use our cache to retrieve sliced shape
-+ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction);
-+ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite());
-+
-+ // see if any of the shapes on their own occludes, only if cached
-+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlockIfCached()) {
- return true;
- }
-+
-+ final boolean firstEmpty = newFirst.isEmpty();
-+ final boolean secondEmpty = newSecond.isEmpty();
-+
-+ if (firstEmpty & secondEmpty) {
-+ return false;
-+ }
-+
-+ if (firstEmpty | secondEmpty) {
-+ return secondEmpty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlock();
-+ }
-+
-+ if (newFirst == newSecond) {
-+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock();
-+ }
-+
-+ return mergedMayOccludeBlock(newFirst, newSecond) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$orUnoptimized(newSecond)).moonrise$occludesFullBlock();
- }
-+ // Paper end - optimise collisions
-+
-+ // Paper start - optimise collisions
-+ public static boolean faceShapeOccludes(final VoxelShape shape1, final VoxelShape shape2) {
-+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlockIfCached()) {
-+ return true;
-+ }
-+
-+ final boolean s1Empty = shape1.isEmpty();
-+ final boolean s2Empty = shape2.isEmpty();
-+ if (s1Empty & s2Empty) {
-+ return false;
-+ }
-+
-+ if (s1Empty | s2Empty) {
-+ return s2Empty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlock();
-+ }
-+
-+ if (shape1 == shape2) {
-+ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock();
-+ }
-
-- public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
-- return one == block()
-- || two == block()
-- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
-+ return mergedMayOccludeBlock(shape1, shape2) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$orUnoptimized(shape2)).moonrise$occludesFullBlock();
-+ // Paper end - optimise collisions
- }
-
- @VisibleForTesting
-diff --git a/net/minecraft/world/phys/shapes/SliceShape.java b/net/minecraft/world/phys/shapes/SliceShape.java
-index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a004617eb53a2 100644
---- a/net/minecraft/world/phys/shapes/SliceShape.java
-+++ b/net/minecraft/world/phys/shapes/SliceShape.java
-@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape {
- super(makeSlice(shape.shape, axis, sliceWidth));
- this.delegate = shape;
- this.axis = axis;
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions
- }
-
- private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
-diff --git a/net/minecraft/world/phys/shapes/VoxelShape.java b/net/minecraft/world/phys/shapes/VoxelShape.java
-index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..6182f1d37c7a63479f6c6e7c37a7edc9cffc3071 100644
---- a/net/minecraft/world/phys/shapes/VoxelShape.java
-+++ b/net/minecraft/world/phys/shapes/VoxelShape.java
-@@ -15,61 +15,546 @@ import net.minecraft.world.phys.AABB;
- import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.Vec3;
-
--public abstract class VoxelShape {
-- protected final DiscreteVoxelShape shape;
-+public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape { // Paper - optimise collisions
-+ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public
- @Nullable
- private VoxelShape[] faces;
-
-+ // Paper start - optimise collisions
-+ private double offsetX;
-+ private double offsetY;
-+ private double offsetZ;
-+ private AABB singleAABBRepresentation;
-+ private double[] rootCoordinatesX;
-+ private double[] rootCoordinatesY;
-+ private double[] rootCoordinatesZ;
-+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData;
-+ private boolean isEmpty;
-+ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs;
-+ private AABB cachedBounds;
-+ private Boolean isFullBlock;
-+ private Boolean occludesFullBlock;
-+
-+ // must be power of two
-+ private static final int MERGED_CACHE_SIZE = 16;
-+ private ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[] mergedORCache;
-+
-+ @Override
-+ public final double moonrise$offsetX() {
-+ return this.offsetX;
-+ }
-+
-+ @Override
-+ public final double moonrise$offsetY() {
-+ return this.offsetY;
-+ }
-+
-+ @Override
-+ public final double moonrise$offsetZ() {
-+ return this.offsetZ;
-+ }
-+
-+ @Override
-+ public final AABB moonrise$getSingleAABBRepresentation() {
-+ return this.singleAABBRepresentation;
-+ }
-+
-+ @Override
-+ public final double[] moonrise$rootCoordinatesX() {
-+ return this.rootCoordinatesX;
-+ }
-+
-+ @Override
-+ public final double[] moonrise$rootCoordinatesY() {
-+ return this.rootCoordinatesY;
-+ }
-+
-+ @Override
-+ public final double[] moonrise$rootCoordinatesZ() {
-+ return this.rootCoordinatesZ;
-+ }
-+
-+ private static double[] extractRawArray(final DoubleList list) {
-+ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) {
-+ final double[] raw = rawList.elements();
-+ final int expected = rawList.size();
-+ if (raw.length == expected) {
-+ return raw;
-+ } else {
-+ return java.util.Arrays.copyOf(raw, expected);
-+ }
-+ } else {
-+ return list.toDoubleArray();
-+ }
-+ }
-+
-+ @Override
-+ public final void moonrise$initCache() {
-+ this.cachedShapeData = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)this.shape).moonrise$getOrCreateCachedShapeData();
-+ this.isEmpty = this.cachedShapeData.isEmpty();
-+
-+ final DoubleList xList = this.getCoords(Direction.Axis.X);
-+ final DoubleList yList = this.getCoords(Direction.Axis.Y);
-+ final DoubleList zList = this.getCoords(Direction.Axis.Z);
-+
-+ if (xList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetX = offsetDoubleList.offset;
-+ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesX = extractRawArray(xList);
-+ }
-+
-+ if (yList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetY = offsetDoubleList.offset;
-+ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesY = extractRawArray(yList);
-+ }
-+
-+ if (zList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetZ = offsetDoubleList.offset;
-+ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesZ = extractRawArray(zList);
-+ }
-+
-+ if (this.cachedShapeData.hasSingleAABB()) {
-+ this.singleAABBRepresentation = new AABB(
-+ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ,
-+ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ
-+ );
-+ this.cachedBounds = this.singleAABBRepresentation;
-+ }
-+ }
-+
-+ @Override
-+ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData() {
-+ return this.cachedShapeData;
-+ }
-+
-+ private VoxelShape[] faceShapeClampedCache;
-+
-+ @Override
-+ public final VoxelShape moonrise$getFaceShapeClamped(final Direction direction) {
-+ if (this.isEmpty) {
-+ return (VoxelShape)(Object)this;
-+ }
-+ if ((VoxelShape)(Object)this == Shapes.block()) {
-+ return (VoxelShape)(Object)this;
-+ }
-+
-+ VoxelShape[] cache = this.faceShapeClampedCache;
-+ if (cache != null) {
-+ final VoxelShape ret = cache[direction.ordinal()];
-+ if (ret != null) {
-+ return ret;
-+ }
-+ }
-+
-+
-+ if (cache == null) {
-+ this.faceShapeClampedCache = cache = new VoxelShape[6];
-+ }
-+
-+ final Direction.Axis axis = direction.getAxis();
-+
-+ final VoxelShape ret;
-+
-+ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
-+ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) {
-+ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1);
-+ } else {
-+ ret = Shapes.empty();
-+ }
-+ } else {
-+ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) {
-+ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0);
-+ } else {
-+ ret = Shapes.empty();
-+ }
-+ }
-+
-+ cache[direction.ordinal()] = ret;
-+
-+ return ret;
-+ }
-+
-+ private boolean computeOccludesFullBlock() {
-+ if (this.isEmpty) {
-+ this.occludesFullBlock = Boolean.FALSE;
-+ return false;
-+ }
-+
-+ if (this.moonrise$isFullBlock()) {
-+ this.occludesFullBlock = Boolean.TRUE;
-+ return true;
-+ }
-+
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB != null) {
-+ // check if the bounding box encloses the full cube
-+ final boolean ret =
-+ (singleAABB.minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
-+ (singleAABB.minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) &&
-+ (singleAABB.minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON));
-+ this.occludesFullBlock = Boolean.valueOf(ret);
-+ return ret;
-+ }
-+
-+ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST);
-+ this.occludesFullBlock = Boolean.valueOf(ret);
-+ return ret;
-+ }
-+
-+ @Override
-+ public final boolean moonrise$occludesFullBlock() {
-+ final Boolean ret = this.occludesFullBlock;
-+ if (ret != null) {
-+ return ret.booleanValue();
-+ }
-+
-+ return this.computeOccludesFullBlock();
-+ }
-+
-+ @Override
-+ public final boolean moonrise$occludesFullBlockIfCached() {
-+ final Boolean ret = this.occludesFullBlock;
-+ return ret != null ? ret.booleanValue() : false;
-+ }
-+
-+ private static int hash(final VoxelShape key) {
-+ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key));
-+ }
-+
-+ @Override
-+ public final VoxelShape moonrise$orUnoptimized(final VoxelShape other) {
-+ // don't cache simple cases
-+ if (((VoxelShape)(Object)this) == other) {
-+ return other;
-+ }
-+
-+ if (this.isEmpty) {
-+ return other;
-+ }
-+
-+ if (other.isEmpty()) {
-+ return (VoxelShape)(Object)this;
-+ }
-+
-+ // try this cache first
-+ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1);
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey];
-+ if (cached != null && cached.key() == other) {
-+ return cached.result();
-+ }
-+
-+ // try other cache
-+ final int otherCacheKey = hash((VoxelShape)(Object)this) & (MERGED_CACHE_SIZE - 1);
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache otherCache = ((VoxelShape)(Object)other).mergedORCache == null ? null : ((VoxelShape)(Object)other).mergedORCache[otherCacheKey];
-+ if (otherCache != null && otherCache.key() == (VoxelShape)(Object)this) {
-+ return otherCache.result();
-+ }
-+
-+ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases
-+ final VoxelShape result = Shapes.joinUnoptimized((VoxelShape)(Object)this, other, BooleanOp.OR);
-+
-+ if (cached != null && otherCache == null) {
-+ // try to use second cache instead of replacing an entry in this cache
-+ if (((VoxelShape)(Object)other).mergedORCache == null) {
-+ ((VoxelShape)(Object)other).mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE];
-+ }
-+ ((VoxelShape)(Object)other).mergedORCache[otherCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache((VoxelShape)(Object)this, result);
-+ } else {
-+ // line is not occupied or other cache line is full
-+ // always bias to replace this cache, as this cache is the first we check
-+ if (this.mergedORCache == null) {
-+ this.mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE];
-+ }
-+ this.mergedORCache[thisCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache(other, result);
-+ }
-+
-+ return result;
-+ }
-+
-+ private static DoubleList offsetList(final double[] src, final double by) {
-+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList wrap = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(src);
-+ if (by == 0.0) {
-+ return wrap;
-+ }
-+ return new OffsetDoubleList(wrap, by);
-+ }
-+
-+ private List<AABB> toAabbsUncached() {
-+ final List<AABB> ret;
-+ if (this.singleAABBRepresentation != null) {
-+ ret = new java.util.ArrayList<>(1);
-+ ret.add(this.singleAABBRepresentation);
-+ } else {
-+ ret = new java.util.ArrayList<>();
-+ final double[] coordsX = this.rootCoordinatesX;
-+ final double[] coordsY = this.rootCoordinatesY;
-+ final double[] coordsZ = this.rootCoordinatesZ;
-+
-+ final double offX = this.offsetX;
-+ final double offY = this.offsetY;
-+ final double offZ = this.offsetZ;
-+
-+ this.shape.forAllBoxes((final int minX, final int minY, final int minZ,
-+ final int maxX, final int maxY, final int maxZ) -> {
-+ ret.add(new AABB(
-+ coordsX[minX] + offX,
-+ coordsY[minY] + offY,
-+ coordsZ[minZ] + offZ,
-+
-+
-+ coordsX[maxX] + offX,
-+ coordsY[maxY] + offY,
-+ coordsZ[maxZ] + offZ
-+ ));
-+ }, true);
-+ }
-+
-+ // cache result
-+ this.cachedToAABBs = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
-+
-+ return ret;
-+ }
-+
-+ private boolean computeFullBlock() {
-+ Boolean ret;
-+ if (this.isEmpty) {
-+ ret = Boolean.FALSE;
-+ } else if ((VoxelShape)(Object)this == Shapes.block()) {
-+ ret = Boolean.TRUE;
-+ } else {
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB == null) {
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
-+ final int sMinX = shapeData.minFullX();
-+ final int sMinY = shapeData.minFullY();
-+ final int sMinZ = shapeData.minFullZ();
-+
-+ final int sMaxX = shapeData.maxFullX();
-+ final int sMaxY = shapeData.maxFullY();
-+ final int sMaxZ = shapeData.maxFullZ();
-+
-+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+
-+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
-+
-+ // index = z + y*sizeZ + x*(sizeZ*sizeY)
-+
-+ final int sizeY = shapeData.sizeY();
-+ final int sizeZ = shapeData.sizeZ();
-+
-+ final long[] bitset = shapeData.voxelSet();
-+
-+ ret = Boolean.TRUE;
-+
-+ check_full:
-+ for (int x = sMinX; x < sMaxX; ++x) {
-+ for (int y = sMinY; y < sMaxY; ++y) {
-+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY);
-+ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) {
-+ ret = Boolean.FALSE;
-+ break check_full;
-+ }
-+ }
-+ }
-+ } else {
-+ ret = Boolean.FALSE;
-+ }
-+ } else {
-+ ret = Boolean.valueOf(
-+ Math.abs(singleAABB.minX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(singleAABB.minY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(singleAABB.minZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+
-+ Math.abs(1.0 - singleAABB.maxX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - singleAABB.maxY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - singleAABB.maxZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON
-+ );
-+ }
-+ }
-+
-+ this.isFullBlock = ret;
-+
-+ return ret.booleanValue();
-+ }
-+
-+ @Override
-+ public final boolean moonrise$isFullBlock() {
-+ final Boolean ret = this.isFullBlock;
-+
-+ if (ret != null) {
-+ return ret.booleanValue();
-+ }
-+
-+ return this.computeFullBlock();
-+ }
-+
-+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) {
-+ final double[] minDistanceArr = new double[] { 1.0 };
-+ final double diffX = to.x - from.x;
-+ final double diffY = to.y - from.y;
-+ final double diffZ = to.z - from.z;
-+
-+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ);
-+
-+ if (direction == null) {
-+ return null;
-+ }
-+
-+ final double minDistance = minDistanceArr[0];
-+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false);
-+ }
-+
-+ private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) {
-+ if (coords.length == 2 &&
-+ DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) &&
-+ DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) {
-+ return (VoxelShape)(Object)this;
-+ }
-+
-+ final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE;
-+
-+ // see findIndex
-+ final int index = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ coords, offset, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)),
-+ 0, coords.length - 1
-+ );
-+
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape(
-+ (VoxelShape)(Object)this, axis, index
-+ );
-+ }
-+ // Paper end - optimise collisions
-+
- protected VoxelShape(DiscreteVoxelShape voxels) {
- this.shape = voxels;
- }
-
- public double min(Direction.Axis axis) {
-- int i = this.shape.firstFull(axis);
-- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i);
-+ // Paper start - optimise collisions
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
-+ switch (axis) {
-+ case X: {
-+ final int idx = shapeData.minFullX();
-+ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
-+ }
-+ case Y: {
-+ final int idx = shapeData.minFullY();
-+ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
-+ }
-+ case Z: {
-+ final int idx = shapeData.minFullZ();
-+ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
-+ }
-+ default: {
-+ // should never get here
-+ return Double.POSITIVE_INFINITY;
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public double max(Direction.Axis axis) {
-- int i = this.shape.lastFull(axis);
-- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i);
-+ // Paper start - optimise collisions
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
-+ switch (axis) {
-+ case X: {
-+ final int idx = shapeData.maxFullX();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
-+ }
-+ case Y: {
-+ final int idx = shapeData.maxFullY();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
-+ }
-+ case Z: {
-+ final int idx = shapeData.maxFullZ();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
-+ }
-+ default: {
-+ // should never get here
-+ return Double.NEGATIVE_INFINITY;
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public AABB bounds() {
-- if (this.isEmpty()) {
-- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
-- } else {
-- return new AABB(
-- this.min(Direction.Axis.X),
-- this.min(Direction.Axis.Y),
-- this.min(Direction.Axis.Z),
-- this.max(Direction.Axis.X),
-- this.max(Direction.Axis.Y),
-- this.max(Direction.Axis.Z)
-- );
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
-+ }
-+ AABB cached = this.cachedBounds;
-+ if (cached != null) {
-+ return cached;
- }
-+
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData;
-+
-+ final double[] coordsX = this.rootCoordinatesX;
-+ final double[] coordsY = this.rootCoordinatesY;
-+ final double[] coordsZ = this.rootCoordinatesZ;
-+
-+ final double offX = this.offsetX;
-+ final double offY = this.offsetY;
-+ final double offZ = this.offsetZ;
-+
-+ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices
-+ cached = new AABB(
-+ coordsX[shapeData.minFullX()] + offX,
-+ coordsY[shapeData.minFullY()] + offY,
-+ coordsZ[shapeData.minFullZ()] + offZ,
-+
-+ coordsX[shapeData.maxFullX()] + offX,
-+ coordsY[shapeData.maxFullY()] + offY,
-+ coordsZ[shapeData.maxFullZ()] + offZ
-+ );
-+
-+ this.cachedBounds = cached;
-+ return cached;
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape singleEncompassing() {
-- return this.isEmpty()
-- ? Shapes.empty()
-- : Shapes.box(
-- this.min(Direction.Axis.X),
-- this.min(Direction.Axis.Y),
-- this.min(Direction.Axis.Z),
-- this.max(Direction.Axis.X),
-- this.max(Direction.Axis.Y),
-- this.max(Direction.Axis.Z)
-- );
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ return Shapes.empty();
-+ }
-+ return Shapes.create(this.bounds());
-+ // Paper end - optimise collisions
- }
-
- protected double get(Direction.Axis axis, int index) {
-- return this.getCoords(axis).getDouble(index);
-+ // Paper start - optimise collisions
-+ final int idx = index;
-+ switch (axis) {
-+ case X: {
-+ return this.rootCoordinatesX[idx] + this.offsetX;
-+ }
-+ case Y: {
-+ return this.rootCoordinatesY[idx] + this.offsetY;
-+ }
-+ case Z: {
-+ return this.rootCoordinatesZ[idx] + this.offsetZ;
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis: " + axis);
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public abstract DoubleList getCoords(Direction.Axis axis);
-
- public boolean isEmpty() {
-- return this.shape.isEmpty();
-+ return this.isEmpty; // Paper - optimise collisions
- }
-
- public VoxelShape move(Vec3 vec3d) {
-@@ -77,24 +562,96 @@ public abstract class VoxelShape {
- }
-
- public VoxelShape move(double x, double y, double z) {
-- return (VoxelShape)(this.isEmpty()
-- ? Shapes.empty()
-- : new ArrayVoxelShape(
-- this.shape,
-- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x),
-- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y),
-- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z)
-- ));
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ return Shapes.empty();
-+ }
-+
-+ final ArrayVoxelShape ret = new ArrayVoxelShape(
-+ this.shape,
-+ offsetList(this.rootCoordinatesX, this.offsetX + x),
-+ offsetList(this.rootCoordinatesY, this.offsetY + y),
-+ offsetList(this.rootCoordinatesZ, this.offsetZ + z)
-+ );
-+
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
-+ if (cachedToAABBs != null) {
-+ ((VoxelShape)(Object)ret).cachedToAABBs = ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs.offset(cachedToAABBs, x, y, z);
-+ }
-+
-+ return ret;
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape optimize() {
-- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()};
-- this.forAllBoxes(
-- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized(
-- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR
-- )
-- );
-- return voxelShapes[0];
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ return Shapes.empty();
-+ }
-+
-+ if (this.singleAABBRepresentation != null) {
-+ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block()
-+ return this.moonrise$isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this;
-+ }
-+
-+ final List<AABB> aabbs = this.toAabbs();
-+
-+ if (aabbs.isEmpty()) {
-+ // We are a SliceShape, which does not properly fill isEmpty for every case
-+ return Shapes.empty();
-+ }
-+
-+ if (aabbs.size() == 1) {
-+ final AABB singleAABB = aabbs.get(0);
-+ final VoxelShape ret = Shapes.create(singleAABB);
-+
-+ // forward AABB cache
-+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) {
-+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs;
-+ }
-+
-+ return ret;
-+ } else {
-+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn)
-+
-+ // set up flat array so that this merge is done in-place
-+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()];
-+
-+ // initialise as unmerged
-+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
-+ tmp[i] = Shapes.create(aabbs.get(i));
-+ }
-+
-+ int size = aabbs.size();
-+ while (size > 1) {
-+ int newSize = 0;
-+ for (int i = 0; i < size; i += 2) {
-+ final int next = i + 1;
-+ if (next >= size) {
-+ // nothing to merge with, so leave it for next iteration
-+ tmp[newSize++] = tmp[i];
-+ break;
-+ } else {
-+ // merge with adjacent
-+ final VoxelShape first = tmp[i];
-+ final VoxelShape second = tmp[next];
-+
-+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
-+ }
-+ }
-+ size = newSize;
-+ }
-+
-+ final VoxelShape ret = tmp[0];
-+
-+ // forward AABB cache
-+ if (((VoxelShape)(Object)ret).cachedToAABBs == null) {
-+ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs;
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - optimise collisions
- }
-
- public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
-@@ -131,9 +688,24 @@ public abstract class VoxelShape {
- }
-
- public List<AABB> toAabbs() {
-- List<AABB> list = Lists.newArrayList();
-- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2)));
-- return list;
-+ // Paper start - optimise collisions
-+ ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
-+ if (cachedToAABBs != null) {
-+ if (!cachedToAABBs.isOffset()) {
-+ return cachedToAABBs.aabbs();
-+ }
-+
-+ // all we need to do is offset the cache
-+ cachedToAABBs = cachedToAABBs.removeOffset();
-+ // update cache
-+ this.cachedToAABBs = cachedToAABBs;
-+
-+ return cachedToAABBs.aabbs();
-+ }
-+
-+ // make new cache
-+ return this.toAabbsUncached();
-+ // Paper end - optimise collisions
- }
-
- public double min(Direction.Axis axis, double from, double to) {
-@@ -155,46 +727,92 @@ public abstract class VoxelShape {
- }
-
- protected int findIndex(Direction.Axis axis, double coord) {
-- return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1;
-+ // Paper start - optimise collisions
-+ final double value = coord;
-+ switch (axis) {
-+ case X: {
-+ final double[] values = this.rootCoordinatesX;
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ values, this.offsetX, value, 0, values.length - 1
-+ );
-+ }
-+ case Y: {
-+ final double[] values = this.rootCoordinatesY;
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ values, this.offsetY, value, 0, values.length - 1
-+ );
-+ }
-+ case Z: {
-+ final double[] values = this.rootCoordinatesZ;
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ values, this.offsetZ, value, 0, values.length - 1
-+ );
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis: " + axis);
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- @Nullable
-- public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) {
-- if (this.isEmpty()) {
-+ // Paper start - optimise collisions
-+ public BlockHitResult clip(final Vec3 from, final Vec3 to, final BlockPos offset) {
-+ if (this.isEmpty) {
- return null;
-- } else {
-- Vec3 vec3 = end.subtract(start);
-- if (vec3.lengthSqr() < 1.0E-7) {
-- return null;
-- } else {
-- Vec3 vec32 = start.add(vec3.scale(0.001));
-- return this.shape
-- .isFullWide(
-- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()),
-- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()),
-- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ())
-- )
-- ? new BlockHitResult(vec32, Direction.getApproximateNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
-- : AABB.clip(this.toAabbs(), start, end, pos);
-+ }
-+
-+ final Vec3 directionOpposite = to.subtract(from);
-+ if (directionOpposite.lengthSqr() < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
-+ return null;
-+ }
-+
-+ final Vec3 fromBehind = from.add(directionOpposite.scale(0.001));
-+ final double fromBehindOffsetX = fromBehind.x - (double)offset.getX();
-+ final double fromBehindOffsetY = fromBehind.y - (double)offset.getY();
-+ final double fromBehindOffsetZ = fromBehind.z - (double)offset.getZ();
-+
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB != null) {
-+ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+ return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
- }
-+ return clip(singleAABB, from, to, offset);
-+ }
-+
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+ return new BlockHitResult(fromBehind, Direction.getApproximateNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
- }
-+
-+ return AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset);
-+ // Paper end - optimise collisions
- }
-
-- public Optional<Vec3> closestPointTo(Vec3 target) {
-- if (this.isEmpty()) {
-+ // Paper start - optimise collisions
-+ public Optional<Vec3> closestPointTo(Vec3 point) {
-+ if (this.isEmpty) {
- return Optional.empty();
-- } else {
-- Vec3[] vec3s = new Vec3[1];
-- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
-- double d = Mth.clamp(target.x(), minX, maxX);
-- double e = Mth.clamp(target.y(), minY, maxY);
-- double f = Mth.clamp(target.z(), minZ, maxZ);
-- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) {
-- vec3s[0] = new Vec3(d, e, f);
-- }
-- });
-- return Optional.of(vec3s[0]);
- }
-+
-+ Vec3 ret = null;
-+ double retDistance = Double.MAX_VALUE;
-+
-+ final List<AABB> aabbs = this.toAabbs();
-+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
-+ final AABB aabb = aabbs.get(i);
-+ final double x = Mth.clamp(point.x, aabb.minX, aabb.maxX);
-+ final double y = Mth.clamp(point.y, aabb.minY, aabb.maxY);
-+ final double z = Mth.clamp(point.z, aabb.minZ, aabb.maxZ);
-+
-+ double dist = point.distanceToSqr(x, y, z);
-+ if (dist < retDistance) {
-+ ret = new Vec3(x, y, z);
-+ retDistance = dist;
-+ }
-+ }
-+
-+ return Optional.ofNullable(ret);
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape getFaceShape(Direction facing) {
-@@ -216,20 +834,24 @@ public abstract class VoxelShape {
- }
- }
-
-- private VoxelShape calculateFace(Direction facing) {
-- Direction.Axis axis = facing.getAxis();
-- if (this.isCubeLikeAlong(axis)) {
-- return this;
-- } else {
-- Direction.AxisDirection axisDirection = facing.getAxisDirection();
-- int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7);
-- SliceShape sliceShape = new SliceShape(this, axis, i);
-- if (sliceShape.isEmpty()) {
-- return Shapes.empty();
-- } else {
-- return (VoxelShape)(sliceShape.isCubeLike() ? Shapes.block() : sliceShape);
-+ private VoxelShape calculateFace(Direction direction) {
-+ // Paper start - optimise collisions
-+ final Direction.Axis axis = direction.getAxis();
-+ switch (axis) {
-+ case X: {
-+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX);
-+ }
-+ case Y: {
-+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY);
-+ }
-+ case Z: {
-+ return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ);
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis: " + axis);
- }
- }
-+ // Paper end - optimise collisions
- }
-
- protected boolean isCubeLike() {
-@@ -249,9 +871,30 @@ public abstract class VoxelShape {
- && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7);
- }
-
-- public double collide(Direction.Axis axis, AABB box, double maxDist) {
-- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist);
-+ // Paper start - optimise collisions
-+ public double collide(final Direction.Axis axis, final AABB source, final double source_move) {
-+ if (this.isEmpty) {
-+ return source_move;
-+ }
-+ if (Math.abs(source_move) < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ switch (axis) {
-+ case X: {
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideX((VoxelShape) (Object) this, source, source_move);
-+ }
-+ case Y: {
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideY((VoxelShape) (Object) this, source, source_move);
-+ }
-+ case Z: {
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideZ((VoxelShape) (Object) this, source, source_move);
-+ }
-+ default: {
-+ throw new RuntimeException("Unknown axis: " + axis);
-+ }
-+ }
- }
-+ // Paper end - optimise collisions
-
- protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) {
- if (this.isEmpty()) {
-diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java
-index 26620c06d26a2c0eb957fbadc6ac3d7a309bff46..3858c83c58e78435a6e29de84c33faa2f26d593d 100644
---- a/net/minecraft/world/ticks/LevelChunkTicks.java
-+++ b/net/minecraft/world/ticks/LevelChunkTicks.java
-@@ -17,7 +17,7 @@ import net.minecraft.core.BlockPos;
- import net.minecraft.nbt.ListTag;
- import net.minecraft.world.level.ChunkPos;
-
--public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T> {
-+public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system
- private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER);
- @Nullable
- private List<SavedTick<T>> pendingTicks;
-@@ -25,6 +25,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
- @Nullable
- private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded;
-
-+ // Paper start - rewrite chunk system
-+ /*
-+ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks
-+ * and the last saved tick is not equal to the current tick
-+ */
-+ /*
-+ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a
-+ * bit out of scope for the chunk system
-+ */
-+
-+ private boolean dirty;
-+ private long lastSaved = Long.MIN_VALUE;
-+
-+ @Override
-+ public final boolean moonrise$isDirty(final long tick) {
-+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved);
-+ }
-+
-+ @Override
-+ public final void moonrise$clearDirty() {
-+ this.dirty = false;
-+ }
-+ // Paper end - rewrite chunk system
-+
- public LevelChunkTicks() {
- }
-
-@@ -49,7 +73,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
- public ScheduledTick<T> poll() {
- ScheduledTick<T> scheduledTick = this.tickQueue.poll();
- if (scheduledTick != null) {
-- this.ticksPerPosition.remove(scheduledTick);
-+ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system
- }
-
- return scheduledTick;
-@@ -58,7 +82,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
- @Override
- public void schedule(ScheduledTick<T> orderedTick) {
- if (this.ticksPerPosition.add(orderedTick)) {
-- this.scheduleUnchecked(orderedTick);
-+ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system
- }
- }
-
-@@ -80,7 +104,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
- while (iterator.hasNext()) {
- ScheduledTick<T> scheduledTick = iterator.next();
- if (predicate.test(scheduledTick)) {
-- iterator.remove();
-+ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system
- this.ticksPerPosition.remove(scheduledTick);
- }
- }
-@@ -110,6 +134,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
- }
-
- public ListTag save(long time, Function<T, String> typeToNameFunction) {
-+ this.lastSaved = time; // Paper - rewrite chunk system
- ListTag listTag = new ListTag();
-
- for (SavedTick<T> savedTick : this.pack(time)) {
-@@ -121,6 +146,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
-
- public void unpack(long time) {
- if (this.pendingTicks != null) {
-+ this.lastSaved = time; // Paper - rewrite chunk system
- int i = -this.pendingTicks.size();
-
- for (SavedTick<T> savedTick : this.pendingTicks) {
-diff --git a/org/bukkit/craftbukkit/CraftChunk.java b/org/bukkit/craftbukkit/CraftChunk.java
-index f3ab07e44e2e912ea66c6148cfdb2a4a528741b2..c2bffe3450ee9f768e00a23ec09df74d7a06d49b 100644
---- a/org/bukkit/craftbukkit/CraftChunk.java
-+++ b/org/bukkit/craftbukkit/CraftChunk.java
-@@ -83,6 +83,12 @@ public class CraftChunk implements Chunk {
- }
-
- public ChunkAccess getHandle(ChunkStatus chunkStatus) {
-+ // Paper start - rewrite chunk system
-+ net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z);
-+ if (full != null) {
-+ return full;
-+ }
-+ // Paper end - rewrite chunk system
- ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus);
-
- // SPIGOT-7332: Get unwrapped extension
-@@ -117,60 +123,12 @@ public class CraftChunk implements Chunk {
-
- @Override
- public boolean isEntitiesLoaded() {
-- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
-+ return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
- }
-
- @Override
- public Entity[] getEntities() {
-- if (!this.isLoaded()) {
-- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
-- }
--
-- PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
-- long pair = ChunkPos.asLong(this.x, this.z);
--
-- if (entityManager.areEntitiesLoaded(pair)) {
-- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
-- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
-- .filter(Objects::nonNull).toArray(Entity[]::new);
-- }
--
-- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
--
-- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
-- ConsecutiveExecutor mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
-- BooleanSupplier supplier = () -> {
-- // only execute inbox if our entities are not present
-- if (entityManager.areEntitiesLoaded(pair)) {
-- return true;
-- }
--
-- if (!entityManager.isPending(pair)) {
-- // Our entities got unloaded, this should normally not happen.
-- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
-- }
--
-- // tick loading inbox, which loads the created entities to the world
-- // (if present)
-- entityManager.tick();
-- // check if our entities are loaded
-- return entityManager.areEntitiesLoaded(pair);
-- };
--
-- // now we wait until the entities are loaded,
-- // the converting from NBT to entity object is done on the main Thread which is why we wait
-- while (!supplier.getAsBoolean()) {
-- if (mailbox.size() != 0) {
-- mailbox.run();
-- } else {
-- Thread.yield();
-- LockSupport.parkNanos("waiting for entity loading", 100000L);
-- }
-- }
--
-- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
-- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
-- .filter(Objects::nonNull).toArray(Entity[]::new);
-+ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system
- }
-
- @Override
-diff --git a/org/bukkit/craftbukkit/CraftServer.java b/org/bukkit/craftbukkit/CraftServer.java
-index 5b64111bc8baca45ecc7bfa384e5f8a004163a0b..97b5d6ba2b19a7c730730c74175a29157aed1840 100644
---- a/org/bukkit/craftbukkit/CraftServer.java
-+++ b/org/bukkit/craftbukkit/CraftServer.java
-@@ -1448,7 +1448,7 @@ public final class CraftServer implements Server {
- // Paper - Put world into worldlist before initing the world; move up
-
- this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
-- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
-+ // Paper - rewrite chunk system
-
- this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
- return internal.getWorld();
-@@ -1493,7 +1493,7 @@ public final class CraftServer implements Server {
- }
-
- handle.getChunkSource().close(save);
-- handle.entityManager.close(save); // SPIGOT-6722: close entityManager
-+ // Paper - rewrite chunk system
- handle.convertable.close();
- } catch (Exception ex) {
- this.getLogger().log(Level.SEVERE, null, ex);
-@@ -2531,7 +2531,7 @@ public final class CraftServer implements Server {
-
- @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 ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); // Paper - rewrite chunk system
- }
-
- // Paper start - Adventure
-diff --git a/org/bukkit/craftbukkit/CraftWorld.java b/org/bukkit/craftbukkit/CraftWorld.java
-index ca62105a0ff0aa69385cbf2018f8fe6a4bb69fd4..92d9f0ea8f7810ae20d3996f49aefa539b4bcb69 100644
---- a/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/org/bukkit/craftbukkit/CraftWorld.java
-@@ -507,15 +507,17 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
- if (playerChunk == null) return false;
-
-- playerChunk.getTickingChunkFuture().thenAccept(either -> {
-- either.ifSuccess(chunk -> {
-+ // Paper start - chunk system
-+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
-+ if (chunk == null) {
-+ return false;
-+ }
-+ // Paper end - chunk system
- List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
-- if (playersInRange.isEmpty()) return;
-+ if (playersInRange.isEmpty()) return true; // Paper - chunk system
-
- FeatureHooks.sendChunkRefreshPackets(playersInRange, chunk);
-- });
-- });
--
-+ // Paper - chunk system
- return true;
- }
-
-@@ -618,20 +620,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- @Override
- public Collection<Plugin> getPluginChunkTickets(int x, int z) {
- DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
--
-- if (tickets == null) {
-- return Collections.emptyList();
-- }
-
-- ImmutableList.Builder<Plugin> ret = ImmutableList.builder();
-- for (Ticket<?> ticket : tickets) {
-- if (ticket.getType() == TicketType.PLUGIN_TICKET) {
-- ret.add((Plugin) ticket.key);
-- }
-- }
--
-- return ret.build();
-+ return chunkDistanceManager.moonrise$getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system
- }
-
- @Override
-@@ -639,7 +629,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
- DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
-
-- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
-+ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
- long chunkKey = chunkTickets.getLongKey();
- SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
-
-@@ -1342,12 +1332,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-
- @Override
- public int getViewDistance() {
-- return this.world.getChunkSource().chunkMap.serverViewDistance;
-+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
- }
-
- @Override
- public int getSimulationDistance() {
-- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
-+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
- }
-
- public BlockMetadataStore getBlockMetadata() {
-@@ -2486,17 +2476,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-
- @Override
- public void setSimulationDistance(final int simulationDistance) {
-- throw new UnsupportedOperationException("Not implemented yet");
-+ if (simulationDistance < 2 || simulationDistance > 32) {
-+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
-+ }
-+ this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system
- }
-
- @Override
- public int getSendViewDistance() {
-- return this.getViewDistance();
-+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
- }
-
- @Override
- public void setSendViewDistance(final int viewDistance) {
-- throw new UnsupportedOperationException("Not implemented yet");
-+ this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system
- }
-
- // Paper start - implement pointers
-diff --git a/org/bukkit/craftbukkit/entity/CraftPlayer.java b/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index e9df37ff66700278bc94ea1e42135b92d97d03f7..6a647cab8b2e476987931486e290703b8726f2c7 100644
---- a/org/bukkit/craftbukkit/entity/CraftPlayer.java
-+++ b/org/bukkit/craftbukkit/entity/CraftPlayer.java
-@@ -3527,7 +3527,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-
- @Override
- public void setViewDistance(final int viewDistance) {
-- throw new UnsupportedOperationException("Not implemented yet");
-+ // Paper - rewrite chunk system - TODO do this better
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+ .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1);
- }
-
- @Override
-@@ -3537,7 +3539,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-
- @Override
- public void setSimulationDistance(final int simulationDistance) {
-- throw new UnsupportedOperationException("Not implemented yet");
-+ // Paper - rewrite chunk system - TODO do this better
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+ .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance);
- }
-
- @Override
-@@ -3547,7 +3551,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-
- @Override
- public void setSendViewDistance(final int viewDistance) {
-- throw new UnsupportedOperationException("Not implemented yet");
-+ // Paper - rewrite chunk system - TODO do this better
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
-+ .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
- }
-
- // Paper start - entity effect API
-diff --git a/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-index 39377ba0739f9660567b38475f101672f7b5e035..c025a4ff42257a4e84f0f9574b84f6987ef8ac11 100644
---- a/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-+++ b/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
-@@ -264,7 +264,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
- return ichunkaccess1;
- };
-
-- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
-+ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system
- }
-
- @Override
-diff --git a/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-index 54c4434662d057a08800918641b95708cda61207..37458e8fd5d57acbf90a6bea4e66797cb07f69fa 100644
---- a/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-+++ b/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-@@ -810,6 +810,13 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
- public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
- return this.handle.getChunkIfLoadedImmediately(x, z);
- }
-+
-+ // Paper start - rewrite chunk system
-+ @Override
-+ public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) {
-+ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate);
-+ }
-+ // Paper end - rewrite chunk system
- // Paper end
- }
-
-diff --git a/org/spigotmc/AsyncCatcher.java b/org/spigotmc/AsyncCatcher.java
-index ef2598760458833021ef1bee92137f42c9fe591f..1f23e775eba1c34e01145bd91b0ce26fed6ca9de 100644
---- a/org/spigotmc/AsyncCatcher.java
-+++ b/org/spigotmc/AsyncCatcher.java
-@@ -9,7 +9,7 @@ public class AsyncCatcher
-
- public static void catchOp(String reason)
- {
-- 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 + "!" );
-diff --git a/org/spigotmc/WatchdogThread.java b/org/spigotmc/WatchdogThread.java
-index ad282d34919716b75acd10426cd071da9d064a51..529df2a41dd93d6e1505053bd04032dbf0cdaa31 100644
---- a/org/spigotmc/WatchdogThread.java
-+++ b/org/spigotmc/WatchdogThread.java
-@@ -8,7 +8,7 @@ import java.util.logging.Logger;
- import net.minecraft.server.MinecraftServer;
- import org.bukkit.Bukkit;
-
--public class WatchdogThread extends Thread
-+public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThread // Paper - rewrite chunk system
- {
-
- private static WatchdogThread instance;
-@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread
- // Paper end - Different message for short timeout
- log.log( Level.SEVERE, "------------------------------" );
- log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
- WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
- log.log( Level.SEVERE, "------------------------------" );
- //
diff --git a/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch b/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch
index 4e1356ce74..6168b4106d 100644
--- a/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch
+++ b/paper-server/patches/features/0020-Rewrite-dataconverter-system.patch
@@ -30560,10 +30560,10 @@ index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98e
+ }
+}
diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 834c5ce238c7adb0164a6282582d709348ef96cc..11cfe9cc29666ce3a6a40281069fb9eb4fa0ded2 100644
+index 0e21efc60e7dd7d348fd024d713772069951ccd4..504a5f8626b42817f04088e2539a6941cd9c6d9d 100644
--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -203,6 +203,43 @@ public final class PaperHooks implements PlatformHooks {
+@@ -204,6 +204,43 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
@Override
public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
final int fromVersion, final int toVersion) {
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch
index 8d1373ef67..b3f729b8b5 100644
--- a/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkHolder.java.patch
@@ -100,7 +100,7 @@
+ chunkResult.ifSuccess(chunk -> {
+ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ ChunkHolder.this.isFullChunkReady = true;
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkBorder(chunk, this);
+ }
+ });
+ });
@@ -111,7 +111,7 @@
if (isOrAfter && !isOrAfter1) {
+ // Paper start
+ if (this.isFullChunkReady) {
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
@@ -126,7 +126,7 @@
+ chunkResult.ifSuccess(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkTicking(chunk, this);
+ });
+ });
+ // Paper end
@@ -137,7 +137,7 @@
- this.tickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isTickingReady) {
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().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
@@ -152,7 +152,7 @@
+ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkEntityTicking(chunk, this);
+ });
+ });
+ // Paper end
@@ -163,7 +163,7 @@
- this.entityTickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isEntityTickingReady) {
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().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
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch
index 6c090dc5e0..06c477be85 100644
--- a/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/ChunkMap.java.patch
@@ -74,10 +74,10 @@
);
stringBuilder.append("Updating:").append(System.lineSeparator());
- this.updatingChunkMap.values().forEach(consumer);
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
stringBuilder.append("Visible:").append(System.lineSeparator());
- this.visibleChunkMap.values().forEach(consumer);
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level).forEach(consumer); // Paper
CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
crashReportCategory.setDetail("Details", details);
@@ -86,7 +86,7 @@
} else {
holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
+ // Paper start
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder);
+ // Paper end
}
@@ -97,7 +97,7 @@
if (flush) {
- List<ChunkHolder> list = this.visibleChunkMap
- .values()
-+ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level) // Paper - moonrise
++ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise
+ //.values() // Paper - moonrise
.stream()
.filter(ChunkHolder::wasAccessibleSinceLastSave)
@@ -107,7 +107,7 @@
long millis = Util.getMillis();
- for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
-+ for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper
++ for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper
this.saveChunkIfNeeded(chunkHolder, millis);
}
}
@@ -115,7 +115,7 @@
public boolean hasWork() {
return this.lightEngine.hasLightWork()
|| !this.pendingUnloads.isEmpty()
-+ || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) // Paper - moonrise
++ || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise
|| !this.updatingChunkMap.isEmpty()
|| this.poiManager.hasWork()
|| !this.toDrop.isEmpty()
@@ -127,7 +127,7 @@
+ // Paper start
+ boolean removed;
+ if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) {
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
+ // Paper end
if (latestChunk instanceof LevelChunk levelChunk) {
levelChunk.setLoaded(false);
@@ -138,7 +138,7 @@
this.nextChunkSaveTime.remove(latestChunk.getPos().toLong());
- }
+ } else if (removed) { // Paper start
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder);
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
+ } // Paper end
}
}, this.unloadQueue::add).whenComplete((_void, error) -> {
@@ -148,7 +148,7 @@
public int size() {
- return this.visibleChunkMap.size();
-+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
++ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolderCount(this.level); // Paper
}
public net.minecraft.server.level.DistanceManager getDistanceManager() {
@@ -157,7 +157,7 @@
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
-+ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
++ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)); // Paper
}
void dumpChunks(Writer writer) throws IOException {
@@ -167,7 +167,7 @@
- for (Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
- long longKey = entry.getLongKey();
-+ for (ChunkHolder entry : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper - Moonrise
++ for (ChunkHolder entry : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper - Moonrise
+ long longKey = entry.pos.toLong(); // Paper - Moonrise
ChunkPos chunkPos = new ChunkPos(longKey);
- ChunkHolder chunkHolder = entry.getValue();
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch
index 99d27344d3..b8d9ca742e 100644
--- a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch
@@ -117,7 +117,7 @@
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
++ ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
+ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+ );
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
index 0d57d512ea..384e9f3c58 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
@@ -34,7 +34,7 @@
+ // I don't want to know why this is a generic type.
+ Entity entityCasted = (Entity)entity;
+ boolean wasRemoved = entityCasted.isRemoved();
-+ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, worldGenSpawned, true);
++ boolean screened = ca.spottedleaf.moonrise.common.PlatformHooks.get().screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, worldGenSpawned, true);
+ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
+ // removed by callback
+ return false;
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
index 6c98d420ea..9b879cbc03 100644
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
@@ -1,5 +1,6 @@
package ca.spottedleaf.moonrise.common;
+import ca.spottedleaf.moonrise.common.util.ChunkSystemHooks;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.DataFixer;
import net.minecraft.core.BlockPos;
@@ -23,7 +24,7 @@ import java.util.List;
import java.util.ServiceLoader;
import java.util.function.Predicate;
-public interface PlatformHooks {
+public interface PlatformHooks extends ChunkSystemHooks {
public static PlatformHooks get() {
return Holder.INSTANCE;
}
@@ -63,8 +64,6 @@ public interface PlatformHooks {
public void entityMove(final Entity entity, final long oldSection, final long newSection);
- public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event);
-
public boolean configFixMC224294();
public boolean configAutoConfigSendDistance();
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
deleted file mode 100644
index 58a99bc38e..0000000000
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+++ /dev/null
@@ -1,288 +0,0 @@
-package ca.spottedleaf.moonrise.common.util;
-
-import ca.spottedleaf.concurrentutil.util.Priority;
-import ca.spottedleaf.moonrise.common.PlatformHooks;
-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();
- 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, Priority.NORMAL);
- }
-
- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
- level.chunkSource.mainThreadProcessor.execute(run);
- }
-
- public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
- final ChunkStatus toStatus, final boolean addTicket, final 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);
- }
- }
- }
- });
- }
-
- 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 Priority priority, final Consumer<ChunkAccess> onComplete) {
- if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
- 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, Priority.HIGHEST);
- });
- }
-
- public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
- final FullChunkStatus toStatus, final boolean addTicket,
- final 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.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
- 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, Priority.HIGHEST);
- });
- }
-
- public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
- return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
- }
-
- public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
- return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
- }
-
- public static int getVisibleChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.visibleChunkMap.size();
- }
-
- public static int getUpdatingChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.updatingChunkMap.size();
- }
-
- public static boolean hasAnyChunkHolders(final ServerLevel level) {
- return getUpdatingChunkHolderCount(level) != 0;
- }
-
- public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) {
- if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) {
- return false;
- }
- return true;
- }
-
- public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
-
- }
-
- public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-
- }
-
- public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-
- }
-
- public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
- }
-
- public static int getSendViewDistance(final ServerPlayer player) {
- return getViewDistance(player);
- }
-
- public static int getViewDistance(final ServerPlayer player) {
- final ServerLevel level = player.serverLevel();
- if (level == null) {
- return org.bukkit.Bukkit.getViewDistance();
- }
- return level.chunkSource.chunkMap.serverViewDistance;
- }
-
- 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;
- }
-
- private ChunkSystem() {}
-}
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java
new file mode 100644
index 0000000000..427079ae47
--- /dev/null
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystemHooks.java
@@ -0,0 +1,77 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.util.Priority;
+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 java.util.List;
+import java.util.function.Consumer;
+
+public interface ChunkSystemHooks {
+
+ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run);
+
+ public void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority);
+
+ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+ final Consumer<ChunkAccess> onComplete);
+
+ public void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete);
+
+ public void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+ final FullChunkStatus toStatus, final boolean addTicket,
+ final Priority priority, final Consumer<LevelChunk> onComplete);
+
+ public List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level);
+
+ public List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level);
+
+ public int getVisibleChunkHolderCount(final ServerLevel level);
+
+ public int getUpdatingChunkHolderCount(final ServerLevel level);
+
+ public boolean hasAnyChunkHolders(final ServerLevel level);
+
+ public boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event);
+
+ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder);
+
+ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder);
+
+ public void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder);
+
+ public ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ);
+
+ public int getSendViewDistance(final ServerPlayer player);
+
+ public int getViewDistance(final ServerPlayer player);
+
+ public int getTickViewDistance(final ServerPlayer player);
+
+ public void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player);
+
+ public void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player);
+
+ public void updateMaps(final ServerLevel world, final ServerPlayer player);
+}
diff --git a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
index 12eb3add09..5239993a68 100644
--- a/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
+++ b/paper-server/src/main/java/ca/spottedleaf/moonrise/common/util/ThreadUnsafeRandom.java
@@ -9,7 +9,7 @@ import net.minecraft.world.level.levelgen.PositionalRandomFactory;
/**
* Avoid costly CAS of superclass
*/
-public final class ThreadUnsafeRandom implements BitRandomSource {
+public class ThreadUnsafeRandom implements BitRandomSource { // Paper - replace random
private static final long MULTIPLIER = 25214903917L;
private static final long ADDEND = 11L;
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 61eac5fbbe..9649f41a95 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -216,7 +216,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
public int getTileEntityCount() {
// We don't use the full world tile entity list, so we must iterate chunks
int size = 0;
- for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world)) {
+ for (ChunkHolder playerchunk : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world)) {
net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
if (chunk == null) {
continue;
@@ -405,7 +405,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
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.common.util.ChunkSystem.scheduleChunkLoad(
+ ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
this.world, x, z, false, ChunkStatus.EMPTY, true, ca.spottedleaf.concurrentutil.util.Priority.NORMAL, future::complete
);
world.getChunkSource().mainThreadProcessor.managedBlock(future::isDone);
@@ -420,7 +420,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Chunk[] getLoadedChunks() {
- List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
+ List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.world); // Paper
return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
}
@@ -2447,7 +2447,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void getChunkAtAsync(int x, int z, boolean gen, boolean urgent, @NotNull Consumer<? super Chunk> cb) {
warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper
- ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+ ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
this.getHandle(), x, z, gen, ChunkStatus.FULL, true,
urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
(ChunkAccess chunk) -> {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 1bdad8088d..039e17ad5d 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -3522,7 +3522,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public int getViewDistance() {
- return ca.spottedleaf.moonrise.common.util.ChunkSystem.getViewDistance(this.getHandle());
+ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getViewDistance(this.getHandle());
}
@Override
@@ -3532,7 +3532,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public int getSimulationDistance() {
- return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
+ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getTickViewDistance(this.getHandle());
}
@Override
@@ -3542,7 +3542,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public int getSendViewDistance() {
- return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
+ return ca.spottedleaf.moonrise.common.PlatformHooks.get().getSendViewDistance(this.getHandle());
}
@Override